超音波センサー + SORACOM Beam でSlack通知

 IoTエンジニア養成読本のハンズオンのラスト、距離を測定するための超音波センサーの測定結果を元にした情報をSORACOM Beamを経由してSlackに通知する処理を実装してみました。

gihyo.jp

超音波センサーの接続

 まずは超音波センサー(HC-SR04)を下記の図のようにRaspberry Piに接続します。動作状況の確認用にLEDも一緒に接続しています。センサーの向きを間違えるとショートしてセンサーやRaspberry Piが壊れる可能性があるということなので注意して配線します。

f:id:akanuma-hiroaki:20170504232728p:plain:w300:left

超音波センサー
赤:2番ピン(+5V) - VCC
黒:20番ピン(GND) - GND
黄:13番ピン(GPIO27) - ECHO
青:11番ピン(GPIO17) - TRIG

LED
黒:6番ピン(GND)
黄:12番ピン(GPIO18)


f:id:akanuma-hiroaki:20170526062708j:plain:w500

距離の測定処理

 まずは距離の測定処理を実装します。超音波センサーでは送信した超音波が物体に当たって跳ね返り、戻ってくるまでにかかる時間から距離を測定しています。超音波センサーの端子としては、送信用がTRIG、受信用がECHOになります。今回はそれぞれGPIO17と27を使用しています。

require 'bundler/setup'
require 'pi_piper'

TRIG_GPIO = 17
ECHO_GPIO = 27

def read_distance(trig_pin_no, echo_pin_no)
  # 送信用(TRIG)、受信用(ECHO)のピンの設定
  trig_pin = PiPiper::Pin.new(pin: trig_pin_no, direction: :out)
  echo_pin = PiPiper::Pin.new(pin: echo_pin_no, direction: :in, trigger: :both)
  trig_pin.off
  sleep(0.3)

  # TRIGに短いパルスを送る
  trig_pin.on
  sleep(0.00001)
  trig_pin.off

  # ECHOがONになる(待ち受け状態になる)まで待ち、時間を記録
  echo_pin.wait_for_change
  signal_off = Time.now

  # ECHOがOFFになる(音波を受信して停止する)まで待ち、時間を記録
  echo_pin.wait_for_change
  signal_on = Time.now

  # 送出時刻と受信時刻の差分を求め、距離を計算する
  time_passed = signal_on - signal_off
  distance = time_passed * 17_000

  # ピンを解放
  PiPiper::Platform.driver.unexport_pin(trig_pin_no)
  PiPiper::Platform.driver.unexport_pin(echo_pin_no)

  return distance if distance <= 500
end

if $0 == __FILE__
  loop do
    start_time = Time.now
    distance = read_distance(TRIG_GPIO, ECHO_GPIO)
    unless distance.nil?
      puts "Distance: %.1f cm" % distance
    end

    wait = start_time + 3 - Time.now
    sleep(wait) if wait > 0
  end
end

 今回使用している距離センサーの使い方は、TRIGに10μs電圧をかけるとパルスが8回送出され、同時にECHOがHIGHになり、音波を受信するとLOWになるとのことでした。

www.switch-science.com

 元のPythonコードではループでECHOのHIGH/LOWの状態を検知して処理していますが、同僚からエッジ検出という手があると聞いて調べたところ、ピンの状態の変化を検知する wait_for_change というメソッドがあったのでこれを使っています。

PiPiper::Pin#wait_for_change
Method: PiPiper::Pin#wait_for_change — Documentation for jwhitehorn/pi_piper (master)

 このメソッドは指定した方向のピンの状態変化を検知するまでそこで待ち、検知するとそれ以降の処理に進みます。wait_for_change のソースは下記のようになっていて、 pin_wait_for というメソッドを呼び出しています。

def wait_for_change
  Platform.driver.pin_wait_for(@pin, @trigger)
end

 pin_wait_for メソッドの実装は下記のようになっていて、ループでピンの状態を検知して、ピンのオブジェクト作成時に trigger オプションに指定した内容次第で、ONになった時、OFFになった時、もしくは両方の場合で変更を検知します。今回は trigger に :both を指定していますので、両方の場合で変更を検知してループを抜けます。

def self.pin_wait_for(pin, trigger)
  pin_set_edge(pin, trigger)

  fd = File.open("/sys/class/gpio/gpio#{pin}/value", "r")
  value = nil
  loop do
    fd.read
    IO.select(nil, nil, [fd], nil)
    last_value = value
    value = self.pin_read(pin)
    if last_value != value
      next if trigger == :rising and value == 0
      next if trigger == :falling and value == 1
      break
    end
  end
end

 自前でループ処理を書かなくても良いのでコードがスッキリするのですが、タイムアウトは設定できません。今回使用している距離センサーはケースによっては状態の変化が起こらなくなってしまうケースもあるようなので、それを考慮すると一定時間でタイムアウトさせて再度測定を開始する必要があります。その場合はやはり自前でループ処理を書いてタイムアウトを設定した方が良いかもしれません。今回はひとまずこのまま pin_for_wait を使った実装で進めてみます。

 これを実行すると下記のように測定結果が出力されていきます。

pi@raspberrypi:~ $ sudo bundle exec ruby distance.rb                                                                                                                                                                                          
Distance: 9.3 cm
Distance: 9.2 cm
Distance: 9.1 cm
Distance: 9.0 cm
Distance: 9.0 cm
Distance: 9.3 cm
Distance: 9.4 cm
Distance: 8.9 cm
Distance: 9.0 cm
Distance: 9.3 cm
Distance: 9.5 cm
Distance: 9.1 cm
Distance: 8.6 cm
Distance: 9.4 cm
Distance: 10.1 cm
Distance: 10.1 cm
Distance: 9.4 cm
Distance: 8.8 cm
Distance: 8.3 cm

 ちなみに書籍で紹介されていたPythonのコードで測定すると、上記と同じ条件で実行しても下記のように測定距離やばらつき度合いが異なります。

pi@raspberrypi:~ $ python distance.py 
Distance: 67.7 cm
Distance: 10.1 cm
Distance: 10.0 cm
Distance: 10.1 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.1 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.7 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.1 cm
Distance: 10.0 cm
Distance: 10.1 cm

 実際の距離は10cm程なので、Pythonの方がばらつきが少なく、実際の距離との誤差も少ないようです。この辺りはECHOでの待ち受け処理やピンの状態検知処理の部分に差があるように思いますが、まだ詳細は調べられてないので、今後時間があるときに調べてみたいと思います。今回はそんなに厳密な測定精度は必要ないので、このままRubyの実装を使用します。

SORACOM Beam + Slackの使用設定

 今回は状態判定結果をSlackへ通知するために、SORACOM Beamを使用します。もちろんRaspberry Piから直接SlackのAPIにリクエストを送ることもできますが、その場合、セキュリティートークンを含むURLをコードの中に書く必要がありますし、IoTデバイスを設置した後で送信先を変更したくなった場合などは設置場所まで行くか、デバイスを回収する等の対応が必要になってしまいます。SORACOM Beamを利用すると、デバイスからはBeamのAPIにリクエストを投げておけば、Beamからアクセスする先のURLはWebコンソールから後からでも変更できますし、その際にセキュリティートークンなどの情報を追加することができます。また、デバイスからBeamへはAir SIMから3G/LTE回線での接続になるので安全に接続できますし、そこから外部のAPIへリクエストを投げる時にTLSによる暗号化を行うことで、デバイス側で暗号化の処理を行う必要もなくなります。

 SORACOM Beamを使うには、ユーザーコンソールからあらかじめ設定しておく必要があります。SIMグループのメニューから該当するグループの「SORACOM Beam設定」をクリックして開き、「+」メニューから「HTTPエントリポイント」を選び、開いたフォームにSlackのIncoming webhook APIのURL等を入力します。ここではSlack側でのIncoming webhook APIの設定方法は割愛します。

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

測定結果からの状態判定 + SORACOM Beamへの送信処理実装

 ここまでで実装した距離の測定処理を使って、測定結果から状態を判定し、今までと状態が変わった場合はSORACOM Beamへリクエストを送信する処理を実装します。また、検知状態がわかるようにLEDのON/OFFも行います。

 弊社の会議室はドアを閉めてしまうと外から中の様子は見えないので、使用中かどうかを示すためにドアに「使用中 / 空室」のプレートを貼ってあるのですが、入退室時に変更を忘れてそのままになってしまって結局使っているのかどうか開けてみないとわからないという状態がよく発生します。そこで今回は距離センサーを使って、会議室のテーブルに誰かいる = 会議室使用中ということで、その状態変化をSlackに通知して、会議室が使われているのかどうかわかるようにしてみたいと思います。

require 'bundler/setup'
require 'httpclient'
require 'logger'
require 'pi_piper'
require './distance.rb'

BEAM_URL = 'http://beam.soracom.io:8888/room'

TRIG_GPIO = 17
ECHO_GPIO = 27
LED_GPIO  = 18
INTERVAL  = 5.0 # sec

logger = Logger.new('room_notify.log')

led_pin  = PiPiper::Pin.new(pin: LED_GPIO,  direction: :out)

occupied = false
state_changed_at = Time.now

threshold = 50 # cm
unless ARGV.empty?
  threshold = ARGV.first.to_f
end

http_client = HTTPClient.new
loop do
  start_time = Time.now
  distance = read_distance(TRIG_GPIO, ECHO_GPIO)

  current_status = distance < threshold
  if occupied != current_status
    duration = Time.now - state_changed_at
    state_changed_at = Time.now
    message = "Distance: %.1f cm - Status changed to %s. (Duration: %d sec)" % [distance, current_status ? 'OCCUPIED' : 'EMPTY', duration]
    payload = '{"text":"%s"}' % message
    res = http_client.post(BEAM_URL, payload, 'Content-Type' => 'application/json')
    logger.info("PAYLOAD: #{payload} / BEAM Response: #{res.status}")
    occupied = current_status
  end

  occupied ? led_pin.on : led_pin.off

  if Time.now < start_time + INTERVAL
    sleep(start_time + INTERVAL - Time.now)
    next
  end
end

 5秒毎にセンサーから距離を読み取り、閾値(デフォルト50cm)を下回った場合は会議室使用中と判定しています。そして前回のステータスと異なっていた場合はSORACOM Beamへリクエストを送信します。また、使用中の場合はLEDを点灯させています。

 これを実行するとセンサーの測定距離に応じてステータスが判定され、ステータスが変更になった場合はSORACOM Beamへリクエストが送信されます。

pi@raspberrypi:~ $ sudo bundle exec ruby room_notify.rb &                                                                                                                                                                                     
[2] 9426

 ログファイルにはPAYLOADとBEAMからのレスポンスを出力しています。

pi@raspberrypi:~ $ tail room_notify.log                                                                                                                                                                                                       
I, [2017-05-24T23:09:50.528556 #5596]  INFO -- : PAYLOAD: {"text":"Distance: 52.7 cm - Status changed to EMPTY. (Duration: 25 sec)"} / BEAM Response: 200
I, [2017-05-24T23:10:05.498609 #5596]  INFO -- : PAYLOAD: {"text":"Distance: 10.1 cm - Status changed to OCCUPIED. (Duration: 14 sec)"} / BEAM Response: 200
I, [2017-05-24T23:18:39.312326 #7964]  INFO -- : PAYLOAD: {"text":"Distance: 9.9 cm - Status changed to OCCUPIED. (Duration: 0 sec)"} / BEAM Response: 200
I, [2017-05-24T23:18:49.242184 #7964]  INFO -- : PAYLOAD: {"text":"Distance: 98.0 cm - Status changed to EMPTY. (Duration: 10 sec)"} / BEAM Response: 200
I, [2017-05-24T23:18:54.314298 #7964]  INFO -- : PAYLOAD: {"text":"Distance: 14.3 cm - Status changed to OCCUPIED. (Duration: 4 sec)"} / BEAM Response: 200
I, [2017-05-24T23:23:33.491639 #9430]  INFO -- : PAYLOAD: {"text":"Distance: 11.3 cm - Status changed to OCCUPIED. (Duration: 0 sec)"} / BEAM Response: 200
I, [2017-05-24T23:23:48.761726 #9430]  INFO -- : PAYLOAD: {"text":"Distance: 54.2 cm - Status changed to EMPTY. (Duration: 15 sec)"} / BEAM Response: 200
I, [2017-05-24T23:23:53.451585 #9430]  INFO -- : PAYLOAD: {"text":"Distance: 46.9 cm - Status changed to OCCUPIED. (Duration: 5 sec)"} / BEAM Response: 200
I, [2017-05-24T23:23:58.791505 #9430]  INFO -- : PAYLOAD: {"text":"Distance: 51.0 cm - Status changed to EMPTY. (Duration: 5 sec)"} / BEAM Response: 200
I, [2017-05-24T23:24:18.531670 #9430]  INFO -- : PAYLOAD: {"text":"Distance: 10.2 cm - Status changed to OCCUPIED. (Duration: 19 sec)"} / BEAM Response: 200

 Slackにも下記のように通知されます。

f:id:akanuma-hiroaki:20170504232956p:plain:w500

 IoTデバイスはWebサービスと違って、設置してしまうとコードを変更したりするのは大変なので、SORACOM Beamのようなサービスで設置後も柔軟に設定を変更できるようになり、同時にセキュリティ面の信頼性も向上できるというのはとても便利だと感じました。

 今回実装したコードは下記リポジトリにも公開してあります。

github.com

温度センサーデータをSORACOM Harvestで可視化する

 引き続きIoTエンジニア養成読本のハンズオンの内容を実践中です。今度は温度センサーのデータを読み取って、そのデータをSORACOM Harvestへ送って可視化する処理をRubyで実装してみます。

gihyo.jp

温度センサーの接続

 まずは下記のように温度センサー(DS18B20)を接続します。温度センサーの3本の端子はそれぞれ用途が決まってるので、向きを間違えないように注意です。抵抗は4.7kΩのものを使っています。

f:id:akanuma-hiroaki:20170518013514p:plain:w300

Raspbianの設定

 次に温度センサーの情報を読み取れるようにRaspbianを設定します。まずは /boot/config.txt に下記差分の内容を追記します。gpioponは温度センサーをどのGPIOに接続するかの指定です。デフォルトが4とのことです。ちなみにここの数字を変えて該当するGPIOに接続を変更してみましたがうまくいかなかったので、変更する場合は他にも変更が必要なのかもしれません。

pi@raspberrypi:~ $ diff /boot/config.txt.20170513 /boot/config.txt
56a57,59
> 
> # Enable thermo sensor (DS18B20+)
> dtoverlay=w1-gpio-pullup,gpiopin=4

 そして /etc/modules にも起動時にモジュールが有効になるように下記差分の内容を追記します。

pi@raspberrypi:~ $ diff /etc/modules.20170513 /etc/modules
5c5,6
< 
---
> w1-gpio
> w1-therm

 設定を有効にするために再起動します。

pi@raspberrypi:~ $ sudo shutdown -r now

温度センサーの計測値を読み出してみる

 Rubyでの処理を実装する前に、まずは直接温度センサーの計測値を表示してみます。ここまでの設定がうまくいっていれば、温度センサーは /sys/bus/w1/devices/28-XXXXXXXXXXXX というディレクトリができ、温度センサーにアクセスできます。XXXXXXXXXXXX の部分は個体によって変わります。

pi@raspberrypi:~ $ ls -l /sys/bus/w1/devices/
total 0
lrwxrwxrwx 1 root root 0 May 13 04:33 28-01162e298eee -> ../../../devices/w1_bus_master1/28-01162e298eee
lrwxrwxrwx 1 root root 0 May 13 04:33 w1_bus_master1 -> ../../../devices/w1_bus_master1
pi@raspberrypi:~ $ 

 計測値を表示するにはデバイスのファイルをcatで開きます。

pi@raspberrypi:~ $ cat /sys/bus/w1/devices/28-*/w1_slave
7e 01 4b 46 7f ff 0c 10 f9 : crc=f9 YES
7e 01 4b 46 7f ff 0c 10 f9 t=23875
pi@raspberrypi:~ $ 

 t=XXXXX の部分が温度データで、摂氏を1,000倍したものが表示されています。

定期的な温度測定&Harvestへの送信処理

 それではこの温度センサーのデータを定期的に読み出して、SORACOM Harvestへデータを送信する処理を書いてみます。

require 'bundler/setup'
require 'httpclient'
require 'logger'

SENSOR_FILE_PATH = "/sys/bus/w1/devices/28-*/w1_slave"
HARVEST_URL = 'http://harvest.soracom.io/'

logger = Logger.new('temperature.log')

interval = 60.0
unless ARGV.empty?
  interval = ARGV.first.to_f
end

device_file_name = Dir.glob(SENSOR_FILE_PATH).first
http_client = HTTPClient.new
loop do
  sensor_data = File.read(device_file_name)
  temperature = sensor_data.match(/t=(.*$)/)[1].to_f / 1000
  payload = '{"temperature":"%.3f"}' % temperature
  res = http_client.post(HARVEST_URL, payload, 'Content-Type' => 'application/json')
  logger.info("PAYLOAD: #{payload} / HARVEST Response: #{res.status}")

  sleep(interval)
end

 Harvestへのデータの送信は、SORACOM Air SIMでネットワークに接続した上で、HarvestのエントリポイントへHTTP、TCPもしくはUDPでデータを送信するだけです。今回はHTTPでJSONデータをPOSTしていますので、Content-Typeには application/json を指定します。1分おきにセンサーデータを読み出し、温度データを1,000で割って摂氏の温度に変換し、Harvestに送信しています。データのPOSTにはhttpclientを使っていますので、gem install等でインストールしておきます。

SORACOM Harvest 使用設定

 Harvestでのデータ集計を利用するにはユーザコンソールで設定をしておく必要があります。SORACOMユーザコンソールの左上のメニューから「グループ」を選択し、該当のSimが所属するグループを開いたら、SORACOM Harvest設定を開き、スライドスイッチをONに切り替えます。

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

 なお、Harvestを有効にすると、書き込み回数に応じて通信料とは別に料金が発生しますのでご注意ください。

soracom.jp

処理の実行

 それでは処理を実行します。SORACOM Harvestを利用するにはSoracom AIR Simからネットワークに接続している必要がありますので、接続した上で下記のように処理を実行します。

pi@raspberrypi:~ $ ruby temperature.rb &
[2] 2939

 ログファイルには下記のように出力されます。

pi@raspberrypi:~ $ tail -f temperature.log 
I, [2017-05-13T06:06:37.734748 #3352]  INFO -- : PAYLOAD: {"temperature":"24.062"} / HARVEST Response: 201
I, [2017-05-13T06:08:12.354801 #3419]  INFO -- : PAYLOAD: {"temperature":"23.875"} / HARVEST Response: 201
I, [2017-05-13T10:50:18.103700 #2915]  INFO -- : PAYLOAD: {"temperature":"26.500"} / HARVEST Response: 201
I, [2017-05-13T10:50:25.385651 #2939]  INFO -- : PAYLOAD: {"temperature":"26.562"} / HARVEST Response: 201
I, [2017-05-13T10:51:27.733886 #2939]  INFO -- : PAYLOAD: {"temperature":"26.375"} / HARVEST Response: 201
I, [2017-05-13T10:52:29.934142 #2939]  INFO -- : PAYLOAD: {"temperature":"26.437"} / HARVEST Response: 201
I, [2017-05-13T10:53:32.343927 #2939]  INFO -- : PAYLOAD: {"temperature":"26.375"} / HARVEST Response: 201
I, [2017-05-13T10:54:34.634204 #2939]  INFO -- : PAYLOAD: {"temperature":"26.625"} / HARVEST Response: 201
I, [2017-05-13T10:55:36.983980 #2939]  INFO -- : PAYLOAD: {"temperature":"26.625"} / HARVEST Response: 201
I, [2017-05-13T10:56:39.254179 #2939]  INFO -- : PAYLOAD: {"temperature":"26.687"} / HARVEST Response: 201

集計データの確認

 SORACOM Harvestで集計されたデータを見るには、ユーザコンソールのSIM Management画面から該当のSIMを選択し、ActionsからHarvest dataを選択します。

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

 すると下記のように集計データがグラフで表示されます。

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

 アップロードされたデータの保存期間は40日間ということなので、継続的にデータを保存する必要があるサービスを構築する場合には自前で環境を用意する必要がありますが、センサーで集めたデータをとりあえず集約してみてみたいというような場合には手軽に使えてとても便利だと思います。

SORACOM Air のメタデータとLEDを連動させる

 引き続きIoTエンジニア養成読本のハンズオンの内容を実践中なわけですが、今度はSORACOM AirのメタデータとLEDの点灯を連動させてる処理をRubyで実装してみます。

gihyo.jp

ユーザーコンソールからの設定

 メタデータサービスを使うにはまずユーザコンソールから、グループ設定とメタデータサービスの使用設定をしておく必要があります。

 ユーザコンソールのメニューからグループを作成した後、そのグループのSORACOM Airの設定で、メタデータサービスの使用設定をONにし、Readonlyのチェックを外して設定を保存します。

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

 次にSORACOM AirのSim管理画面から、該当のSimのグループを先ほど作成したグループに変更して設定を保存します。

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

f:id:akanuma-hiroaki:20170507161630p:plain:w500

SORACOM Air のメタデータサービスに接続

 まずはSORACOM Airで3Gネットワークに接続します。

pi@raspberrypi:~ $ sudo /usr/local/sbin/connect_air.sh &
[1] 30291
pi@raspberrypi:~ $ Found AK-020
waiting for modem device
--> WvDial: Internet dialer version 1.61
--> Cannot get information for serial port.
--> Initializing modem.
--> Sending: ATZ
ATZ
OK
--> Sending: ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
OK
--> Sending: AT+CGDCONT=1,"IP","soracom.io"
AT+CGDCONT=1,"IP","soracom.io"
OK
--> Modem initialized.
--> Sending: ATD*99***1#
--> Waiting for carrier.
ATD*99***1#
OK
CONNECT 21000000
--> Carrier detected.  Starting PPP immediately.
--> Starting pppd at Thu May  4 05:36:43 2017
--> Pid of pppd: 30314
--> Using interface ppp0
--> local  IP address 10.247.81.162
--> remote IP address 10.64.64.64
--> primary   DNS address 100.127.0.53
--> secondary DNS address 100.127.1.53

 続いてcurlで直接メタデータサービスに接続してみます。(レスポンスの内容は省略しています)

pi@raspberrypi:~ $ curl http://metadata.soracom.io/v1/subscriber
{...}

 SORACOM Airでネットワークに接続していないとメタデータサービスにはアクセスできません。

 レスポンスのjsonを整形してみると下記のようになります。(一部マスクしています)

{
    "apn": "soracom.io", 
    "createdAt": 1493055578551, 
    "createdTime": 1493055578551, 
    "expiredAt": null, 
    "expiryAction": null, 
    "expiryTime": null, 
    "groupId": "d79b3210-8d23-4a41-8003-6b6acdec5e55", 
    "iccid": "XXXXXXXXXXXXXXXXXX", 
    "imeiLock": null, 
    "imsi": "XXXXXXXXXXXXX", 
    "ipAddress": "10.XXX.XX.XXX", 
    "lastModifiedAt": 1493876507132, 
    "lastModifiedTime": 1493876507132, 
    "moduleType": "nano", 
    "msisdn": "XXXXXXXXXXX", 
    "operatorId": "OPXXXXXXXXX", 
    "plan": 0, 
    "serialNumber": "AXXXXXXXXXXXXX", 
    "sessionStatus": {
        "dnsServers": [
            "100.127.0.53", 
            "100.127.1.53"
        ], 
        "gatewayPublicIpAddress": "54.XXX.XXX.XX", 
        "imei": "XXXXXXXXXXXXX", 
        "lastUpdatedAt": 1493876215192, 
        "location": null, 
        "online": true, 
        "ueIpAddress": "10.XXX.XX.XXX"
    }, 
    "speedClass": "s1.slow", 
    "status": "active", 
    "tags": {}, 
    "terminationEnabled": false, 
    "type": "s1.slow"
}

 取得できるデータの特定の項目だけ取得したい場合は下記のようにURLの末尾に項目名を追加します。

pi@raspberrypi:~ $ curl http://metadata.soracom.io/v1/subscriber.type
s1.slow

メタデータの更新

 メタデータのタグはAPIで取得だけでなく更新も可能です。curlでJSON形式でPUTリクエストを送信してみます。

pi@raspberrypi:~ $ curl http://metadata.soracom.io/v1/subscriber.tags
{}
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ curl -X PUT -H content-type:application/json -d '[{"tagName":"led","tagValue" :"off"}]' http://metadata.soracom.io/v1/subscriber/tags
{...}
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ curl http://metadata.soracom.io/v1/subscriber.tags
{"led":"off"}
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ curl http://metadata.soracom.io/v1/subscriber.tags.led
off
pi@raspberrypi:~ $ 

メタデータのタグ情報によってLEDの状態を変更する

 それではRubyスクリプトからメタデータサービスにアクセスして、タグの内容によってLEDを点灯/消灯してみます。

require 'bundler/setup'
require 'pi_piper'
require 'open-uri'

interval = 60.0
unless ARGV.empty?
  interval = ARGV[0].to_f
end

led_pin = PiPiper::Pin.new(pin: 22, direction: :out)

loop do
  print 'Connecting to Meta-data service... '
  begin
    res = OpenURI.open_uri('http://metadata.soracom.io/v1/subscriber.tags.led', read_timeout: 5)
    code = res.status.first.to_i

    if code != 200
      puts "ERROR: Invalid response code: #{code} message: #{res.status[1]}"
      break
    end

    led_tag = res.read.rstrip
    if led_tag.downcase == 'off'
      puts 'LED tag is OFF. Turn off the LED.'
      led_pin.off
    elsif led_tag.downcase == 'on'
      puts 'LED tag is ON. Turn on the LED.'
      led_pin.on
    end
  rescue => e
    puts e.message
    puts e.backtrace.join("\n")
    break
  end

  if interval > 0
    sleep(interval)
    next
  end

  break
end

 実行結果は下記のようになります。最初はonだったタグ情報をユーザコンソールからoffに変更すると、LEDが消灯されます。またonに変更すると、LEDが点灯されます。

pi@raspberrypi:~ $ sudo bundle exec ruby degital_twin1.rb 10
Connecting to Meta-data service... LED tag is ON. Turn on the LED.
Connecting to Meta-data service... LED tag is ON. Turn on the LED.
Connecting to Meta-data service... LED tag is ON. Turn on the LED.
Connecting to Meta-data service... LED tag is ON. Turn on the LED.
Connecting to Meta-data service... LED tag is OFF. Turn off the LED. ← この直前にユーザコンソールからタグの値をoffに変更
Connecting to Meta-data service... LED tag is OFF. Turn off the LED.
Connecting to Meta-data service... LED tag is ON. Turn on the LED. ← この直前にユーザコンソールからタグの値をonに変更
Connecting to Meta-data service... LED tag is ON. Turn on the LED.

LEDの状態をメタデータに反映する

 次は、スイッチが押されたらLEDのON/OFFを切り替え、その情報をメタデータの方にも反映します。

require 'bundler/setup'
require 'pi_piper'
require 'net/http'
require 'open-uri'

interval = 60.0
unless ARGV.empty?
  interval = ARGV[0].to_f
end

led_pin = PiPiper::Pin.new(pin: 22, direction: :out)
led_pin.off

switch_pin = PiPiper::Pin.new(pin: 23, direction: :in, pull: :up)

start_time = nil

loop do
  start_time = Time.now.to_i

  print '- Connecting to Meta-data service... '
  begin
    res = OpenURI.open_uri('http://metadata.soracom.io/v1/subscriber.tags.led', read_timeout: 5)
    code = res.status.first.to_i

    if code != 200
      puts "ERROR: Invalid response code: #{code} message: #{res.status[1]}"
      break
    end

    led_tag = res.read.rstrip
    if led_tag.downcase == 'off'
      puts 'LED tag is OFF. Turn off the LED.'
      led_pin.off
    elsif led_tag.downcase == 'on'
      puts 'LED tag is ON. Turn on the LED.'
      led_pin.on
    end
  rescue => e
    puts e.message
    puts e.backtrace.join("\n")
    break
  end

  puts "- Waiting input via the switch (%.1f sec)" % (start_time + interval - Time.now.to_i)
  loop do
    switch_pin.read
    led_pin.read
    if switch_pin.value == 0
      puts "The switch has been pushed. Turn %s the LED" % (led_pin.off? ? 'ON' : 'OFF')
      led_pin.off? ? led_pin.on : led_pin.off
      led_pin.read

      print 'Updating Meta-data... '
      payload = '[{"tagName":"led","tagValue":"%s"}]' % (led_pin.on? ? 'on' : 'off')
      uri = URI.parse('http://metadata.soracom.io/v1/subscriber/tags')
      http = Net::HTTP.new(uri.host, uri.port)
      req = Net::HTTP::Put.new(uri.path, initheader = { 'Content-Type' => 'application/json'})
      req.body = payload
      res = http.start {|http| http.request(req) }
      puts "response_code: #{res.code}"
    end

    if Time.now.to_i > start_time + interval
      break
    end

    sleep(0.1)
  end
end

 実行結果は下記の通りです。30秒ごとにメタデータサービスからタグ情報を取得していますが、その間にスイッチが押された場合はその情報をメタデータサービスに反映しています。

pi@raspberrypi:~ $ sudo bundle exec ruby degital_twin2.rb 30
- Connecting to Meta-data service... LED tag is ON. Turn on the LED.
- Waiting input via the switch (29.0 sec)
- Connecting to Meta-data service... LED tag is ON. Turn on the LED.
- Waiting input via the switch (29.0 sec)
The switch has been pushed. Turn OFF the LED ← スイッチを押下
Updating Meta-data... response_code: 200
- Connecting to Meta-data service... LED tag is OFF. Turn off the LED.
- Waiting input via the switch (29.0 sec)
The switch has been pushed. Turn ON the LED ← スイッチを押下
Updating Meta-data... response_code: 200
- Connecting to Meta-data service... LED tag is ON. Turn on the LED.
- Waiting input via the switch (29.0 sec)

Raspberry Pi + RubyでLチカ

 前回でRaspberry Piの初期設定がだいたい終わったので、引き続きIoTエンジニア養成読本のハンズオンの内容をベースにLチカ(LED点滅)をやってみました。

gihyo.jp

 書籍の例ではPythonが使われていますが、そのままやっても面白くないのでRubyで挑戦しました。mrubyでやりたかったのですが、mrubyでGPIOを操作するためのライブラリのmruby-WiringPiとmruby-raspberryを試してみたもののビルドがうまくいかなかったので、ひとまずCRubyでやってみます。

まずは常時点灯

 まずは特に制御はせずに、常時LEDが点灯するように接続してみます。下記の図のように接続するとLEDが点灯します。書籍で使われている抵抗は330Ωですが、購入した抵抗セットの中には含まれてなかったので、1kΩのものを使っています。また、回路図は Fritzing を使って作成しました。

Fritzing
http://fritzing.org/home/

f:id:akanuma-hiroaki:20170504133715p:plain:w300:left

f:id:akanuma-hiroaki:20170504135253j:plain:w300

赤:1番ピン
黒:6番ピン(GND)

コマンドラインからLED点灯

 次にコマンドラインから直接GPIIOを操作してみます。GPIOへの入出力はOSの擬似ファイルとして提供されているようなので、それを操作します。

 まず使用するGPIOのピンをexportファイルに対して設定します。今回はGPIO22のピンを使います。

f:id:akanuma-hiroaki:20170504134511p:plain:w300:left

黒:15番ピン(GPIO22)
赤:6番ピン(GND)


pi@raspberrypi:~ $ ls -l /sys/class/gpio/export 
-rwxrwx--- 1 root gpio 4096 May  1 05:36 /sys/class/gpio/export
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ file /sys/class/gpio/export                                                                                                                                                                                                
/sys/class/gpio/export: ERROR: cannot read `/sys/class/gpio/export' (Input/output error)
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ cat /sys/class/gpio/export                                                                                                                                                                                                 
cat: /sys/class/gpio/export: Input/output error
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ echo 22 > /sys/class/gpio/export                                                                                                                                                                                           
pi@raspberrypi:~ $ 

 するとそのGPIOピンを操作するためのディレクトリが作られるので、まずは入出力の向きを指定します。今回はLEDの点灯のための出力なので、outを設定します。

pi@raspberrypi:~ $ ls -l /sys/class/gpio/gpio22/direction 
-rwxrwx--- 1 root gpio 4096 May  1 06:16 /sys/class/gpio/gpio22/direction
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ file /sys/class/gpio/gpio22/direction                                                                                                                                                                                      
/sys/class/gpio/gpio22/direction: ASCII text
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ cat /sys/class/gpio/gpio22/direction                                                                                                                                                                                       
in
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ echo out > /sys/class/gpio/gpio22/direction                                                                                                                                                                                
pi@raspberrypi:~ $ 

 そしてそのピンの値を1にすると点灯状態になり、0にすると消灯状態に戻ります。

pi@raspberrypi:~ $ ls -l /sys/class/gpio/gpio22/value 
-rwxrwx--- 1 root gpio 4096 May  1 06:16 /sys/class/gpio/gpio22/value
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ file /sys/class/gpio/gpio22/value 
/sys/class/gpio/gpio22/value: ASCII text
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ cat /sys/class/gpio/gpio22/value 
0
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ echo 1 > /sys/class/gpio/gpio22/value 
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ echo 0 > /sys/class/gpio/gpio22/value 
pi@raspberrypi:~ $ 

RubyスクリプトでLチカ

 ではRubyスクリプトでLチカをやってみたいと思います。まずはRaspberry Pi上にRuby環境を作るために、gitとrbenvと、その他必要なライブラリをインストールします。

pi@raspberrypi:~ $ sudo apt-get install git
pi@raspberrypi:~ $ sudo apt-get install rbenv
pi@raspberrypi:~ $ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> .bashrc
pi@raspberrypi:~ $ echo 'eval "$(rbenv init -)"' >> .bashrc
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ tail .bashrc
# sources /etc/bash.bashrc).
if ! shopt -oq posix; then
  if [ -f /usr/share/bash-completion/bash_completion ]; then
    . /usr/share/bash-completion/bash_completion
  elif [ -f /etc/bash_completion ]; then
    . /etc/bash_completion
  fi
fi
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
Cloning into '/home/pi/.rbenv/plugins/ruby-build'...
remote: Counting objects: 7534, done.
remote: Compressing objects: 100% (48/48), done.
remote: Total 7534 (delta 36), reused 0 (delta 0), pack-reused 7483
Receiving objects: 100% (7534/7534), 1.54 MiB | 850.00 KiB/s, done.
Resolving deltas: 100% (4567/4567), done.
Checking connectivity... done.
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ sudo apt-get install -y libssl-dev libreadline-dev

 そしてruby2.4.1をインストール。

pi@raspberrypi:~ $ rbenv install 2.4.1                                                                                                                                                                                                        
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
        LANGUAGE = (unset),
        LC_ALL = (unset),
        LC_CTYPE = "UTF-8",
        LANG = "en_GB.UTF-8"
    are supported and installed on your system.
perl: warning: Falling back to a fallback locale ("en_GB.UTF-8").
Downloading ruby-2.4.1.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.1.tar.bz2
Installing ruby-2.4.1...
Installed ruby-2.4.1 to /home/pi/.rbenv/versions/2.4.1
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ rbenv global 2.4.1
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ rbenv version
2.4.1 (set by /home/pi/.rbenv/version)
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ ruby -v
ruby 2.4.1p111 (2017-03-22 revision 58053) [armv7l-linux-eabihf]

 gemの管理にはbundlerを使います。

pi@raspberrypi:~ $ gem install bundler
Fetching: bundler-1.14.6.gem (100%)
Successfully installed bundler-1.14.6
Parsing documentation for bundler-1.14.6
Installing ri documentation for bundler-1.14.6
Done installing documentation for bundler after 27 seconds
1 gem installed
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ bundle init
Writing new Gemfile to /home/pi/Gemfile
pi@raspberrypi:~ $ 

 GPIOの操作にはroot権限が必要なので、sudoでbundlerが使えるように、visudoでrbenvのパスを追加しておきます。

pi@raspberrypi:~ $ sudo visudo

secure_path に /home/pi/.rbenv/shims を追加

 そして今回はRubyからGPIOを操作するために、Pi Piper を使用してみます。

github.com

 Gemfileに pi_piper の記述を追加してbundle installします。

pi@raspberrypi:~ $ vi Gemfile 
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ cat Gemfile
# frozen_string_literal: true
source "https://rubygems.org"

gem 'pi_piper'
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ bundle install --path vendor/bundle
Fetching gem metadata from https://rubygems.org/...........
Fetching version metadata from https://rubygems.org/.
Resolving dependencies...
Installing eventmachine 1.0.9 with native extensions
Installing ffi 1.9.18 with native extensions
Using bundler 1.14.6
Installing pi_piper 2.0.0
Bundle complete! 1 Gemfile dependency, 4 gems now installed.
Bundled gems are installed into ./vendor/bundle.
pi@raspberrypi:~ $ 

 ここまででスクリプトを実行する準備はできたので、スクリプトを用意します。内容は下記の通り。

require 'bundler/setup'
require 'pi_piper'

pin = PiPiper::Pin.new(pin: 22, direction: :out)

loop do
  pin.on
  sleep 1
  pin.off
  sleep 1
end

 GPIOナンバーと入出力の方向を指定してPinオブジェクトを生成し、onメソッドで点灯、offメソッドで消灯しています。実行にはroot権限が必要なので、sudoでbundler経由でスクリプトを実行します。

pi@raspberrypi:~ $ sudo bundle exec ruby led-flash.rb

youtu.be

スイッチの入力検出

 では次にタクトスイッチを追加して、その入力を検出してみたいと思います。

f:id:akanuma-hiroaki:20170504135109p:plain:w300:left

f:id:akanuma-hiroaki:20170504135454j:plain:w300

赤:15番ピン(GPIO22)
黒:6番ピン(GND)
黄:16番ピン(GPIO23)


require 'bundler/setup'                                    
require 'pi_piper'                                         
                                                           
pin = PiPiper::Pin.new(pin: 23, direction: :in, pull: :up) 
                                                           
loop do                                                    
  pin.read                                                 
  if pin.value == 0                                        
    puts 'The switch has been pushed.'                     
  end                                                      
  sleep 1                                                  
end                                                        

 GPIOナンバーは23で、入出力の向きはinを指定します。プルアップ・プルダウン抵抗はプルアップを指定します。readメソッドで入力値を読み取って、ボタンが押されたら(valueが0だったら)コンソールにメッセージを出力します。

スイッチ押下中だけLED点灯

 次にスイッチの入力とLEDへの出力を組み合わせて、スイッチを押している間だけLEDを点灯させます。

require 'bundler/setup'                                            
require 'pi_piper'                                                 
                                                                   
led_pin = PiPiper::Pin.new(pin: 22, direction: :out)               
switch_pin = PiPiper::Pin.new(pin: 23, direction: :in, pull: :up)  
                                                                   
loop do                                                            
  switch_pin.read                                                  
  if switch_pin.off?                                               
    led_pin.on                                                     
  else                                                             
    led_pin.off                                                    
  end                                                              
  sleep(0.5)                                                       
end                                                                

 スイッチの入力値の関係がまだよくわかっていないのですが、押された時はvalueが0になり、off?メソッドがtrueになるので、その場合はLEDを点灯しています。

スイッチ押下でLEDのON/OFF切り替え

 そして最後にスイッチを押すことでLEDのON/OFFを切り替えるようにしてみます。

require 'bundler/setup'                                                                       
require 'pi_piper'                                                                            
                                                                                              
led_pin = PiPiper::Pin.new(pin: 22, direction: :out)                                          
led_pin.off                                                                                   
                                                                                              
switch_pin = PiPiper::Pin.new(pin: 23, direction: :in, pull: :up)                             
                                                                                              
loop do                                                                                       
  switch_pin.read                                                                             
  if switch_pin.value == 0                                                                    
    puts "Turn %s the LED since the switch has been pushed." % (led_pin.off? ? 'ON' : 'OFF')  
    led_pin.off? ? led_pin.on : led_pin.off                                                   
    led_pin.read                                                                              
  end                                                                                         
  sleep(0.5)                                                                                  
end                                                                                           

 スイッチが押されたらLEDの点灯状態を反転させています。ON/OFF切り替え後にreadメソッドを実行しないと現在の状態が認識されないようだったので、実行しています。

 Pi Piperではループを回して待ち受けるのではなく、watch や after メソッドを使ってイベントドリブンな形で実装することもできるようなのですが、ちょっと試した限りではうまくいかなかったので、いずれそちらの形で実装できるようにしてみたいと思います。

Raspberry Pi + SORACOM Air セットアップ

 前からRaspberry PiやArduino等に興味はあったものの自分では試せていなかったのですが、先日 IoT Technology Conference if-up 2017 で 3G SIM の USBドングルをいただいたので、これを機に自分でもRaspberry Piを購入して色々と試してみることにしました。同じカンファレンスで紹介されていてとても良さそうだったIoTエンジニア養成読本も買って、まずはそのハンズオンの内容をベースに動くものを作ってみようと思います。

gihyo.jp

Raspberry Piのセットアップ

 まずは下記URLからRaspbian OSのイメージをダウンロードします。HDMIモニタやUSBキーボードを接続せずにセットアップしたかったので、デフォルトでsshサーバが起動する2016-09-28のイメージを選択しました。これより後のバージョンだとデフォルトではsshサーバは起動しないようです。(結局LANケーブルが見つからずHDMIモニタとUSBキーボードを繋いでセットアップしたので、最新のを選択してもよかったのですが。。)

http://ftp.jaist.ac.jp/pub/raspberrypi/raspbian_lite/images/raspbian_lite-2016-09-28/

 ダウンロードしたファイルは解凍してimgファイルを取り出しておきます。

 続いてイメージファイルをSDカードに書き込んでいきます。今回は16GBのマイクロSDにアダプタをつけてMacBook ProのSDカードスロットにさします。

f:id:akanuma-hiroaki:20170501134021j:plain:w300

 diskutilコマンドでストレージを確認します。

pi  $ diskutil list
/dev/disk0 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *500.3 GB   disk0
   1:                        EFI EFI                     209.7 MB   disk0s1
   2:          Apple_CoreStorage Macintosh HD            499.4 GB   disk0s2
   3:                 Apple_Boot Recovery HD             650.0 MB   disk0s3

/dev/disk1 (internal, virtual):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:                            Macintosh HD           +499.1 GB   disk1
                                 Logical Volume on disk0s2
                                 D522C0D0-F775-4496-8BDA-640948662DCD
                                 Unlocked Encrypted

/dev/disk2 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *15.5 GB    disk2
   1:             Windows_FAT_32 NO NAME                 15.5 GB    disk2s1

 サイズから判断して /dev/disk2 がSDカードなので、ddコマンドでSDカードにOSのイメージを書き込みます。マウントしていると書き込めないので、diskutil unmountDisk コマンドでアンマウントしてから実行します。

pi  $ sudo dd if=2016-09-23-raspbian-jessie-lite.img of=/dev/rdisk2 bs=1m
Password:
dd: /dev/rdisk2: Resource busy
pi  $ 
pi  $ diskutil unmountDisk /dev/disk2
Unmount of all volumes on disk2 was successful
pi  $ 
pi  $ sudo dd if=2016-09-23-raspbian-jessie-lite.img of=/dev/rdisk2 bs=1m
1325+0 records in
1325+0 records out
1389363200 bytes transferred in 94.477593 secs (14705743 bytes/sec)
pi  $ 

 MacBook ProからSDカードを取り出してアダプタを外し、Raspberry PiのSDカードスロットにさします。今回買ったのはRaspberry Pi 3 Model Bです。

f:id:akanuma-hiroaki:20170501134726j:plain:w500

 HDMIモニタとUSBキーボードを接続して最後に電源ケーブルを接続すると、OSが起動します。

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

 ログインプロンプトが表示されたらデフォルトのログインIDとパスワードでログインし、無線LANへの接続の設定を行います。wpa_passphraseコマンドを利用するのですが、wpa_supplicant.confはrootユーザしか書き込み権限を持っていないので、書籍でも紹介されているようにパイプでつないで tee コマンドをsudoで使って書き込むのが良いのですが、手元のキーボードだとパイプが入力できず、キーボードの設定を変更すれば良いと思うのですが面倒だったので、一時的にroot以外にも書き込み権限を与えてリダイレクトでファイルに追記し、そのあとでまた権限を戻しました。MY_AP_SSID と MY_AP_PASSWORD は接続するAPのSSIDとパスワードに置き換えてください。設定後はOSを再起動します。

$ sudo chmod 606 /etc/wpa_supplicant/wpa_supplicant.conf
$ wpa_passphrase MY_AP_SSID MY_AP_PASSWORD >> /etc/wpa_supplicant/wpa_supplicant.conf
$ cat /etc/wpa_supplicant/wpa_supplicant.conf
$ sudo chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf
$ sudo reboot

 再起動後にipコマンドで無線LANへ接続できていてIPアドレスが割り当てられていることを確認します。

pi@raspberrypi:~ $ ip a show dev wlan0
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether b8:27:eb:e6:89:f8 brd ff:ff:ff:ff:ff:ff
    inet 192.168.10.10/24 brd 192.168.10.255 scope global wlan0
       valid_lft forever preferred_lft forever
    inet6 2408:212:2862:5c00:b342:9793:7abe:c897/64 scope global noprefixroute dynamic 
       valid_lft 2591786sec preferred_lft 604586sec
    inet6 fe80::bc23:73fb:4394:7972/64 scope link 
       valid_lft forever preferred_lft forever

 同じLAN内のRaspberry Pi端末には raspberrypi.local でアクセスできるので、下記のようにMacBook Proからsshでログインします。

pi  $ ssh pi@raspberrypi.local
The authenticity of host 'raspberrypi.local (2408:212:2862:5c00:b342:9793:7abe:c897)' can't be established.
ECDSA key fingerprint is SHA256:B98CBwQsKcPnKMeIGBNQ065GNnMXZvBm1pKoHc7+0Zw.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'raspberrypi.local,2408:212:2862:5c00:b342:9793:7abe:c897' (ECDSA) to the list of known hosts.
pi@raspberrypi.local's password: 

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sat Apr 29 16:26:21 2017

 下記コマンドでRaspbianを最新の状態にアップデートします。

% sudo apt-get update
% sudo apt-get upgrade
% sudo apt-get dist-upgrade

 最後にpasswdコマンドでデフォルトのログインパスワードをオリジナルのものに変更して、基本的なセットアップは終了です。

SORACOM Air での接続セットアップ

 カンファレンスでいただいた3G Sim の USBドングルを使って、SORACOM Airでネットワーク接続できるようにセットアップします。事前にSORACOM AirのSimを購入して、SORACOMのユーザアカウントの作成とSimの登録を済ませておきます。

f:id:akanuma-hiroaki:20170501140826j:plain:w500

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

 いただいたUSBドングルはABIT AK-020です。

f:id:akanuma-hiroaki:20170501141204j:plain:w500

 3G接続に必要なパッケージをインストールします。

pi@raspberrypi:~ $ sudo apt-get install -y usb-modeswitch wvdial
Reading package lists... Done
Building dependency tree       
Reading state information... Done
usb-modeswitch is already the newest version.
The following extra packages will be installed:
  libpcap0.8 libuniconf4.6 libwvstreams4.6-base libwvstreams4.6-extras ppp
The following NEW packages will be installed:
  libpcap0.8 libuniconf4.6 libwvstreams4.6-base libwvstreams4.6-extras ppp wvdial
0 upgraded, 6 newly installed, 0 to remove and 0 not upgraded.
Need to get 1390 kB of archives.
After this operation, 3127 kB of additional disk space will be used.
Get:1 http://mirrordirector.raspbian.org/raspbian/ jessie/main libpcap0.8 armhf 1.6.2-2 [121 kB]
Get:2 http://mirrordirector.raspbian.org/raspbian/ jessie/main libwvstreams4.6-base armhf 4.6.1-7 [235 kB]
Get:3 http://mirrordirector.raspbian.org/raspbian/ jessie/main libwvstreams4.6-extras armhf 4.6.1-7 [448 kB]
Get:4 http://mirrordirector.raspbian.org/raspbian/ jessie/main libuniconf4.6 armhf 4.6.1-7 [173 kB]
Get:5 http://mirrordirector.raspbian.org/raspbian/ jessie/main ppp armhf 2.4.6-3.1 [306 kB]
Get:6 http://mirrordirector.raspbian.org/raspbian/ jessie/main wvdial armhf 1.61-4.1 [107 kB]
Fetched 1390 kB in 2s (493 kB/s)
Can't set locale; make sure $LC_* and $LANG are correct!
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
        LANGUAGE = (unset),
        LC_ALL = (unset),
        LC_CTYPE = "UTF-8",
        LANG = "en_GB.UTF-8"
    are supported and installed on your system.
perl: warning: Falling back to a fallback locale ("en_GB.UTF-8").
locale: Cannot set LC_CTYPE to default locale: No such file or directory
locale: Cannot set LC_ALL to default locale: No such file or directory
Preconfiguring packages ...
Selecting previously unselected package libpcap0.8:armhf.
(Reading database ... 31414 files and directories currently installed.)
Preparing to unpack .../libpcap0.8_1.6.2-2_armhf.deb ...
Unpacking libpcap0.8:armhf (1.6.2-2) ...
Selecting previously unselected package libwvstreams4.6-base.
Preparing to unpack .../libwvstreams4.6-base_4.6.1-7_armhf.deb ...
Unpacking libwvstreams4.6-base (4.6.1-7) ...
Selecting previously unselected package libwvstreams4.6-extras.
Preparing to unpack .../libwvstreams4.6-extras_4.6.1-7_armhf.deb ...
Unpacking libwvstreams4.6-extras (4.6.1-7) ...
Selecting previously unselected package libuniconf4.6.
Preparing to unpack .../libuniconf4.6_4.6.1-7_armhf.deb ...
Unpacking libuniconf4.6 (4.6.1-7) ...
Selecting previously unselected package ppp.
Preparing to unpack .../ppp_2.4.6-3.1_armhf.deb ...
Unpacking ppp (2.4.6-3.1) ...
Selecting previously unselected package wvdial.
Preparing to unpack .../wvdial_1.61-4.1_armhf.deb ...
Unpacking wvdial (1.61-4.1) ...
Processing triggers for man-db (2.7.0.2-5) ...
Processing triggers for systemd (215-17+deb8u6) ...
Setting up libpcap0.8:armhf (1.6.2-2) ...
Setting up libwvstreams4.6-base (4.6.1-7) ...
Setting up libwvstreams4.6-extras (4.6.1-7) ...
Setting up libuniconf4.6 (4.6.1-7) ...
Setting up ppp (2.4.6-3.1) ...
Setting up wvdial (1.61-4.1) ...
locale: Cannot set LC_CTYPE to default locale: No such file or directory
locale: Cannot set LC_ALL to default locale: No such file or directory

Sorry.  You can retry the autodetection at any time by running "wvdialconf".
   (Or you can create /etc/wvdial.conf yourself.)
Processing triggers for libc-bin (2.19-18+deb8u7) ...
Processing triggers for systemd (215-17+deb8u6) ...

 SORACOM Airでネットワーク接続するためのスクリプトをダウンロードして実行権限を付与します。

pi@raspberrypi:~ $ curl -O http://soracom-files.s3.amazonaws.com/connect_air.sh
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2843  100  2843    0     0  23012      0 --:--:-- --:--:-- --:--:-- 23113
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ ls -l
total 4
-rw-r--r-- 1 pi pi 2843 Apr 30 04:08 connect_air.sh
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ chmod +x connect_air.sh 
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ ls -l
total 4
-rwxr-xr-x 1 pi pi 2843 Apr 30 04:08 connect_air.sh
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ sudo mv connect_air.sh /usr/local/sbin/
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ ls -l /usr/local/sbin/connect_air.sh 
-rwxr-xr-x 1 pi pi 2843 Apr 30 04:08 /usr/local/sbin/connect_air.sh

 そしてUSBドングルにSimを入れて、Raspberry PiのUSBポートに挿し、スクリプトを実行します。

pi@raspberrypi:~ $ sudo /usr/local/sbin/connect_air.sh 
Found AK-020
Configuring modem ... done.
waiting for modem device..done.
Resetting modem ...done
could not initialize AK-020
waiting for modem device
--> WvDial: Internet dialer version 1.61
--> Cannot get information for serial port.
--> Initializing modem.
--> Sending: ATZ
ATZ
OK
--> Sending: ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
OK
--> Sending: AT+CGDCONT=1,"IP","soracom.io"
AT+CGDCONT=1,"IP","soracom.io"
OK
--> Modem initialized.
--> Sending: ATD*99***1#
--> Waiting for carrier.
ATD*99***1#
CONNECT 21000000
--> Carrier detected.  Starting PPP immediately.
--> Starting pppd at Sun Apr 30 04:19:32 2017
--> Pid of pppd: 1873
--> Using interface ppp0
--> local  IP address 10.247.81.162
--> remote IP address 10.64.64.64
--> primary   DNS address 100.127.0.53
--> secondary DNS address 100.127.1.53

 SORACOMのユーザーコンソールから確認すると、SessionがOnlineになっていて、接続できていることが確認できます。

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

ifup されてきました(if-up2017に参加してきました)

 先日ソラコムさん主催の IoT Technology Conference if-up 2017 に行ってきました。ifup というのはLinuxでネットワークインタフェースを有効にするためのコマンドで、参加者それぞれの IoT Technology に関するインタフェースをUPしてほしいということでカンファレンスのタイトルに使われたそうです。セッションの内容はどれも興味深く、参考になるものばかりで、まんまと私も ifup されてきたわけですが、特にソラコムCTOの安川さんによる、「SORACOM Inside」という、SORACOMの設計思想やアーキテクチャを紹介されたキーノートの内容がとても参考になったので、その中で特に印象深かった点を紹介させていただきます。

f:id:akanuma-hiroaki:20170430000154j:plain:w300f:id:akanuma-hiroaki:20170430000202j:plain:w500

 安川さんのセッション資料はこちらで公開されています。

www.slideshare.net

Polaris, Dipper, Hubble

 SORACOMプラットフォームは、パケット転送等の主要機能を担う Polaris(北極星)、認証や課金などの周辺機能を担う Dipper(北斗七星)、監視やデプロイを担う Hubble(宇宙望遠鏡)という要素で構成されているそう。プロダクトの役割と思想がマッチしたこういうネーミングはかっこいいなーと思いました。内部のメンバーとしても、それぞれのプロダクトの役割について共通の認識が持てるのではないかと思います。

ローンチ時からグローバル対応

 SORACOMユーザーコンソールはローンチ時からグローバル対応していて、HTML/JS/CSSによるSPAとして構成し、APIでSORACOMプラットフォームと連携し、S3にアップロードすることでCloudFrontで世界に配信されるようにしているとのこと。また、最初から多言語に対応し、タイムゾーンはUTCに統一しているとのことでした。一度普通のWebアプリとして構成してしまったものを後からSPAの構成に変更するのは大変そうですが、最初からこの構成を意識していれば、確かにS3にアップロードするだけでユーザーコンソールアプリがデプロイできるのは良いなと思いました。タイムゾーンもJSTをベースにしようとすると、色々な箇所でミスなくJSTに統一するのも大変ですし、UTCに統一することができれば後から世界展開するときにも確かに面倒なことが少なそうです。

疎結合化と非同期化

 SORACOMでは各サービス間の連携はs3を経由するなどして疎結合化されていて、一方に障害が起こっても、他方に直接的な影響がないようにしているとのこと。また、データ形式さえ決めておけば、それぞれのサービスの開発も非同期で行えるので、開発速度の向上にも有効なようです。いわゆるマイクロサービスの構成かと思いますが、前述のSPAの構成と同様で、一度モノリシックな構成で作ってしまうと後からマイクロサービスに分けるのは現実的ではなかったりします。スタートアップのサービス立ち上げ当初は特にモノリシックな一つのWebアプリとして一気に作ってしまうケースが多いかと思いますが、マイクロサービスの構成を最初から意識して作られている点はすごいと思いました。

DevOpsとOpsDev

 ソラコムの開発者は全員DevOpsを実践されているそうですが、運用の守りが手薄にならないよう、運用作業省力化のための開発を専門的に行うOpsDevエンジニアを導入されているそう。Hubbleの中でも障害を検知した時は、インスタンスの入れ替えで復旧できるようなものは、自動的に復旧させるようにしているとのことです。検知から通知まではどこの会社でも普通に実施していると思いますが、自動復旧までやれているケースはあまりないのではないでしょうか。また、OpsDevという考え方は聞いたことがありませんでしたが、運用省力化のための開発が好きなエンジニアがいるケースであれば、有効な選択肢だと思いました。うちの会社でも取り入れてみようかと思っています。

当たり前をちゃんとやれてるのがすごい

 講演後に安川さんとお話しさせていただいたときに、「内容としては当たり前のことばかりなので、講演前は聞いている方の反応がどうなるか不安があった」ということでした。マイクロサービスの考え方など、確かに一つ一つは広まっているものも多いですが、それぞれをしっかりとやり切れているというのはなかなかないと思いますし、とても参考になりました。また、システムの構成以外にも、ソラコムさんではリーダーシップ・ステートメントや、毎日行われているSyncというミーティングなど、チームアップの面でも参考にさせていただきたい点が多いので、またお話させていただきたいなぁと思っています。

 それと、今回参加者には参加特典としてIoTデバイスがプレゼントされていて、私は3G SIMのUSBドングルをもらいました。そこでこれを機にRaspberry Piも買ったので、色々やってみようと思います。

 今回一つ心残りだったのは、休憩時間中にまつもとゆきひろさんと玉川さんと3人でお話させていただく機会があったのですが、なかなかこのお二人と一緒に話をさせてもらう機会はないと思うので、3人で写真を撮らせてもらえば良かったと後になって思いました。次に機会があれば撮らせていただこうと思っています。

f:id:akanuma-hiroaki:20170430000122j:plain:w300

各種パラメータ最適化手法の実装(SGD, Momentum, AdaGrad, Adam)

 今回は「ゼロから作るDeepLearning」で紹介されている各種パラメータ最適化手法を、書籍のPythonのサンプルコードをベースに、Rubyで実装してみました。

www.oreilly.co.jp

 各手法のロジックについては書籍で説明されていますので割愛します。また、前回の記事で書いたように、Rubyでは値の受け渡しが参照の値渡しになるので、パラメータのハッシュの各値は配列として保持する前提です。

SGD(確率的勾配降下法)

 SGDは前回の記事でもすでに使っていたのと同じで、別クラスとして分けただけのものです。

class SGD
  def initialize(lr: 0.01)
    @lr = lr
  end

  def update(params:, grads:)
    params.keys.each do |key|
      params[key][0] -= @lr * grads[key]
    end
  end
end

Momentum

 paramsのハッシュの各値を配列として扱っている以外は、PythonのコードをそのままRubyに置き換えています。インスタンス変数 v にはparamsと同じ構造の値を持ちますが、インスタンス内でしか使わないため、ハッシュの値は配列にはせずにそのままパラメータを保持しています。

 ゼロ行列の生成は Numo::NArray.zeros メソッドを使っています。

Numo::NArray.zeros
http://ruby-numo.github.io/narray/narray/Numo/NArray.html#zeros-class_method

class Momentum
  def initialize(lr: 0.01, momentum: 0.9)
    @lr = lr
    @momentum = momentum
    @v = nil
  end

  def update(params:, grads:)
    if @v.nil?
      @v = {}
      params.each do |key, value|
        @v[key] = Numo::DFloat.zeros(value.first.shape)
      end
    end

    params.keys.each do |key|
      @v[key] = @momentum * @v[key] - @lr * grads[key]
      params[key][0] += @v[key]
    end
  end
end

AdaGrad

 こちらもMomentumと同様にPythonのコードを置き換えています。

 行列に対しての平方根の計算は、Numo::DFloat::Math.sqrt メソッドを使っています。

Numo::DFloat::Math.sqrt
http://ruby-numo.github.io/narray/narray/Numo/DFloat/Math.html#sqrt-class_method

class AdaGrad
  def initialize(lr: 0.01)
    @lr = lr
    @h = nil
  end

  def update(params:, grads:)
    if @h.nil?
      @h = {}
      params.each do |key, value|
        @h[key] = Numo::DFloat.zeros(value.first.shape)
      end
    end

    params.keys.each do |key|
      @h[key] += grads[key] * grads[key]
      params[key][0] -= @lr * grads[key] / (Numo::DFloat::Math.sqrt(@h[key]) + 1e-7)
    end
  end
end

Adam

 こちらも同様の置き換えです。

class Adam
  def initialize(lr: 0.001, beta1: 0.9, beta2: 0.999)
    @lr = lr
    @beta1 = beta1
    @beta2 = beta2
    @iter = 0
    @m = nil
    @v = nil
  end

  def update(params:, grads:)
    if @m.nil?
      @m = {}
      @v = {}
      params.each do |key, value|
        @m[key] = Numo::DFloat.zeros(value.first.shape)
        @v[key] = Numo::DFloat.zeros(value.first.shape)
      end
    end

    @iter += 1
    lr_t = @lr * Numo::DFloat::Math.sqrt(1.0 - @beta2 ** @iter) / (1.0 - @beta1 ** @iter)

    params.keys.each do |key|
      @m[key] += (1 - @beta1) * (grads[key] - @m[key])
      @v[key] += (1 - @beta2) * (grads[key] ** 2 - @v[key])

      params[key][0] -= lr_t * @m[key] / (Numo::DFloat::Math.sqrt(@v[key]) + 1e-7)
    end
  end
end

MNISTデータセットによる最適化手法の比較

 上記の最適化手法の実装について、MNISTデータセットを用いた学習の比較を行います。こちらも基本的には書籍のPython実装をベースにしていて、5 層のニューラルネットワークで、各層 100 個のニューロンを持つ ネットワークという構成です。活性化関数にはReLUを用いています。

 まずは複数レイヤのネットワークの処理を行うためのMultiLayerNetクラスの実装です。以前のTwoLayerNetを、3層以上のネットワークにも対応させた形です。

require 'numo/narray'
require './layers.rb'

class MultiLayerNet
  def initialize(input_size:, hidden_size_list:, output_size:, activation: :relu, weight_init_std: :relu, weight_decay_lambda: 0)
    @input_size          = input_size
    @output_size         = output_size
    @hidden_size_list    = hidden_size_list
    @hidden_layer_num    = hidden_size_list.size
    @weight_decay_lambda = weight_decay_lambda
    @params              = {}

    # 重みの初期化
    init_weight(weight_init_std)

    # レイヤの生成
    activation_layer = {
      sigmoid: Sigmoid,
      relu:    Relu
    }
    @layers = {}
    (1..@hidden_layer_num).each do |idx|
      @layers["Affine#{idx}"] = Affine.new(w: @params["w#{idx}"], b: @params["b#{idx}"])
      @layers["Activation_function#{idx}"] = activation_layer[activation].new
    end

    idx = @hidden_layer_num + 1
    @layers["Affine#{idx}"] = Affine.new(w: @params["w#{idx}"], b: @params["b#{idx}"])

    @last_layer = SoftmaxWithLoss.new
  end

  def params
    @params
  end

  def init_weight(weight_init_std)
    all_size_list = [@input_size] + @hidden_size_list + [@output_size]
    (1..(all_size_list.size - 1)).each do |idx|
      scale = weight_init_std
      if %i(relu he).include?(weight_init_std)
        scale = Numo::DFloat::Math.sqrt(2.0 / all_size_list[idx - 1])
      elsif %i(sigmoid xavier).include?(weight_init_std)
        scale = Numo::DFloat::Math.sqrt(1.0 / all_size_list[idx - 1])
      end

      Numo::NArray.srand
      @params["w#{idx}"] = [scale * Numo::DFloat.new(all_size_list[idx - 1], all_size_list[idx]).rand_norm]
      @params["b#{idx}"] = [Numo::DFloat.zeros(all_size_list[idx])]
    end
  end

  def predict(x:)
    @layers.values.inject(x) do |x, layer|
      x = layer.forward(x: x)
    end
  end

  # x: 入力データ, t: 教師データ
  def loss(x:, t:)
    y = predict(x: x)

    weight_decay = 0
    (1..(@hidden_layer_num + 1)).each do |idx|
      w = @params["w#{idx}"].first
      weight_decay += 0.5 * @weight_decay_lambda * (w ** 2).sum
    end
    @last_layer.forward(x: y, t: t) + weight_decay
  end

  def accuracy(x:, t:)
    y = predict(x: x)
    y = y.max_index(1) % 10
    if t.ndim != 1
      t = t.max_index(1) % 10
    end

    y.eq(t).cast_to(Numo::UInt16).sum / x.shape[0].to_f
  end

  def gradient(x:, t:)
    # forward
    loss(x: x, t: t)

    # backward
    dout = 1
    dout = @last_layer.backward(dout: dout)

    layers = @layers.values.reverse
    layers.inject(dout) do |dout, layer|
      dout = layer.backward(dout: dout)
    end

    grads = {}
    (1..(@hidden_layer_num + 1)).each do |idx|
      grads["w#{idx}"] = @layers["Affine#{idx}"].dw + @weight_decay_lambda * @layers["Affine#{idx}"].w.first
      grads["b#{idx}"] = @layers["Affine#{idx}"].db
    end

    grads
  end
end

 そして各手法での学習と、結果のグラフ描画を行うためのスクリプトの実装です。基本的な処理は前回の誤差逆伝播法での学習処理と同じで、SGD以外にもMomentum、AdaGrad、Adamでの学習を行い、結果を比較しています。

require 'numo/gnuplot'
require './mnist.rb'
require './optimizers.rb'
require './multi_layer_net.rb'

# 0: MNISTデータの読み込み
x_train, t_train, x_test, t_test = load_mnist(normalize: true)

train_size = x_train.shape[0]
batch_size = 128
max_iterations = 1500

# 1: 実験の設定
optimizers = {
  sgd:      SGD.new,
  momentum: Momentum.new,
  adagrad:  AdaGrad.new,
  adam:     Adam.new
}

networks = {}
train_loss = {}
optimizers.each do |key, optimizer|
  networks[key]   = MultiLayerNet.new(input_size: 784, hidden_size_list: [100, 100, 100, 100], output_size: 10)
  train_loss[key] = []
end

# 2: 訓練の開始
max_iterations.times do |i|
  Numo::NArray.srand
  batch_mask = Numo::Int32.new(batch_size).rand(0, train_size)
  x_batch = x_train[batch_mask, true]
  t_batch = t_train[batch_mask]

  optimizers.each do |key, optimizer|
    grads = networks[key].gradient(x: x_batch, t: t_batch)
    optimizers[key].update(params: networks[key].params, grads: grads)

    loss = networks[key].loss(x: x_batch, t: t_batch)
    train_loss[key] << loss
  end

  next unless i % 100 == 0

  puts "========== iteration: #{i} =========="
  optimizers.keys.each do |key|
    loss = networks[key].loss(x: x_batch, t: t_batch)
    puts "#{key}: #{loss}"
  end
end

# 3: グラフの描画
x = (0..(max_iterations - 1)).to_a
Numo.gnuplot do
  set xlabel: 'iterations'
  set ylabel: 'loss'
  set yrange: 0...1
  plot x, train_loss[:sgd],      { w: :lines, t: 'SGD',      lc_rgb: 'green',  lw: 1 },
       x, train_loss[:momentum], { w: :lines, t: 'Momentum', lc_rgb: 'orange', lw: 1 },
       x, train_loss[:adagrad],  { w: :lines, t: 'AdaGrad',  lc_rgb: 'red',    lw: 1 },
       x, train_loss[:adam],     { w: :lines, t: 'Adam',     lc_rgb: 'blue',   lw: 1 }
end

 これをirbから実行すると下記のようにコンソールに結果が100イテレーションごとに表示され、最後にグラフが描画されます。

irb(main):001:0> load './optimizer_compare_mnist.rb'
========== iteration: 0 ==========
sgd: 2.490228492804417
momentum: 2.492025460726973
adagrad: 2.0825493921580134
adam: 2.276848398313694
========== iteration: 100 ==========
sgd: 1.5001470235084333
momentum: 0.42706984858496944
adagrad: 0.2004084345838115
adam: 0.34096154614776963
========== iteration: 200 ==========
sgd: 0.7664658855425125
momentum: 0.2733076953685949
adagrad: 0.11048856792711875
adam: 0.24828428524427817
========== iteration: 300 ==========
sgd: 0.5159466996794285
momentum: 0.25594092908625543
adagrad: 0.13587365073017388
adam: 0.20545236418926946
========== iteration: 400 ==========
sgd: 0.5042479159281286
momentum: 0.24538385847033825
adagrad: 0.11124732792005954
adam: 0.1797581753729203
========== iteration: 500 ==========
sgd: 0.32290967978019125
momentum: 0.1599522422679423
adagrad: 0.05731788233265379
adam: 0.0888823836035264
========== iteration: 600 ==========
sgd: 0.44467997494741673
momentum: 0.2578398459161452
adagrad: 0.1316129116477675
adam: 0.22439066383913017
========== iteration: 700 ==========
sgd: 0.28407622085704987
momentum: 0.10056311655844065
adagrad: 0.07989693533502204
adam: 0.0821317626167635
========== iteration: 800 ==========
sgd: 0.2706466278682429
momentum: 0.15550352523100197
adagrad: 0.06489312962717893
adam: 0.08528336870483003
========== iteration: 900 ==========
sgd: 0.2240422184352822
momentum: 0.11062658792202897
adagrad: 0.059913720263603615
adam: 0.03302573552710864
========== iteration: 1000 ==========
sgd: 0.3832020077768542
momentum: 0.13726781722583942
adagrad: 0.03417701415203686
adam: 0.053558781255080776
========== iteration: 1100 ==========
sgd: 0.38619949224379774
momentum: 0.15175966760909282
adagrad: 0.04222220798211423
adam: 0.06822940475295906
========== iteration: 1200 ==========
sgd: 0.2998755819916694
momentum: 0.07572742725924923
adagrad: 0.075962654676941
adam: 0.02748595912322749
========== iteration: 1300 ==========
sgd: 0.25322815781566416
momentum: 0.06003606774412698
adagrad: 0.03236788855975958
adam: 0.053987864918752786
========== iteration: 1400 ==========
sgd: 0.2720482764348912
momentum: 0.09105631835160209
adagrad: 0.044338756972504875
adam: 0.0668196066196452
=> true

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

 イテレーションの回数を1,500回としていますが、手元のVM環境ではこれ以上回数を増やすと途中で強制終了されてしまいました。Pythonコードをそのままの構成で移植しているので、もっとRubyに最適化した実装を考慮する必要がありそうです。

 Numo.gnuplot でのグラフ描画時は、setメソッドによる設定はplotより前に実行しておかないとグラフに反映されないようでした。

 コードは下記リポジトリにも公開しています。

github.com