PyObjCとからCoreBluetoothを呼び出しWx2Beaconのデータを取得する。
この記事で紹介すること
この記事ではMacOSXのBluetoothライブラリである、CoreBluethoothをPythonから呼び出し、WxBeacon2(OMRON 2JCIE-BL01)から環境データを取得する方法について解説しています。 CoreBlutoothはObjectiveCのライブラリのため、PythonとObjectiveCのブリッジライブラリのPyObjCを利用しています。 以下の順番で説明します。
- WxBeacon2の概要+BLEの基礎
- PyObjeCのインストール
- WxBeacon2の読み出しプログラム
WxBeacon2
前回の記事PythonからBLEを制御するライブラリの調査でも記載しましたが、WxBeacon2はウェザーニュースから販売されているBLEインタフェースの環境センサです。OMRONの2JCIE-BL01とほぼ同等の製品になります。外観は以下のようになっており、WxBeacon2にはウェザーニュースさんのロゴがプリントされているのが特徴です。
センサとして取得できる項目は以下の通りです、
センサ名 | 単位 |
---|---|
温度センサ | ℃ |
湿度センサ | % |
気圧センサ | hPa |
照度センサ | lum |
紫外線センサ | ? |
騒音センサ(マイク) | dB |
またこれ以外の値としてバッテリーの値が取得できます。 WxBeacon2はBLEにおいて、ペリフェラルとして動作します。またはBeaconのブロードキャスターとしても動作します。今回はWxBeacon2をBLEペリフェラルとして接続します。 話は脱線しますが、BLEの通信について少し記載しておきます。
BLEの通信について
BLEの通信ではセントラルとペリフェラルという2つの役割があります。BLEを使うユースケースの例としてスマートフォンと活動量計を考えます。たいていの場合、スマートフォンがセントラル、活動量計がペリフェラルとなります。
セントラル
親機のようなイメージ。ペリフェラルを発見後接続要求を送り、データをやりとりを行う。また、ペリフェラルへNotificationの依頼を送れる。
ペリフェラル
アドバタイズと呼ばれるデータを送信しラントラルに発見させます。 ペリフェラルの機能は大分類のサービスと小分類のキャラクタリスティックで定義されています。さらに個々のキャラクタリスティックのデータに対してRead, Write, Notifyのどの操作が可能か定義されてます。キャラクタリスティックはデータが格納されているアドレスと考えるとすんなりわかります。
セントラルとペリフェラルの通信の流れ
通信は以下のような流れになります。
- ペリフェラルがアドバタイズと呼ばれるデータをブロードキャストする。
- ペリフェラルのアドバタイズを受信したセントラルが接続を要求する。
- ペリフェラルに接続したセントラルがペリフェラルのサービスとキャラクタリスティックを検索
- 目的のキャラクタリスティックにRead Write Notifyなどの処理を行う。
PyObjCのインストール
PyObjCはPythonからOSXのObjectiveCのAPIへアクセスするためのブリッジです。
インストール方法
pipからインストールできます。私の環境OSX 10.11.4 El Capitanではlibffiを入れ、パスを通す必要がありました。
$brew install pkg-config libffi $export PKG_CONFIG_PATH=/usr/local/Cellar/libffi/3.0.13/lib/pkgconfig/ $pip install pyobjc
ソースコード
PyObjCを利用してCoreBluetoothを呼び出し、WxBeacon2からデータを取得するまでのコードを解説します。以下にソースコードを置いています。
最初に必要なモジュールをインポートします。
#!/usr/bin/env python # encoding: utf-8 import struct from Foundation import * from PyObjCTools import AppHelper
from Foundation import *
にてCoreBluetoothを含むObjectiveCのライブラリを読み込みます。from PyObjeCTools import AppHelper
でアプリケーションを実行する
ヘルパー関数を読み込みます。
次にアプリケーションのメイン関数部分です。
if "__main__" == __name__: central_manager = CBCentralManager.alloc()#(1) central_manager.initWithDelegate_queue_options_(BleClass(), None, None)#(2) AppHelper.runConsoleEventLoop()#(3)
- (1) CBCentralManagerのインスタンスを取得
- (2) (1)で取得したインスタンスを初期化する。その際、コールバックを設定しているBleClassを設定
- (3) アプリケーションを実行すうスレッドを起動する。
wx2_service = CBUUID.UUIDWithString_(u'0C4C3000-7700-46F4-AA96-D5E974E32A54') wx2_characteristic_data = CBUUID.UUIDWithString_(u'0C4C3001-7700-46F4-AA96-D5E974E32A54') class BleClass(object): def centralManagerDidUpdateState_(self, manager): self.manager = manager manager.scanForPeripheralsWithServices_options_(None,None) #(1) def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(self, manager, peripheral, data, rss): #(2) self.peripheral = peripheral if '8A783AEE-4277-4C3F-8382-ABFA4F6DB8B6' in repr(peripheral.UUID): print 'DeviceName' + peripheral.name() manager.connectPeripheral_options_(peripheral, None) manager.stopScan() def centralManager_didConnectPeripheral_(self, manager, peripheral): #(3) print repr(peripheral.UUID()) peripheral.setDelegate_(self) self.peripheral.discoverServices_([wx2_service]) def peripheral_didDiscoverServices_(self, peripheral, services): #(4) self.service = self.peripheral.services()[0] self.peripheral.discoverCharacteristics_forService_([wx2_characteristic_data], self.service) def peripheral_didDiscoverCharacteristicsForService_error_(self, peripheral, service, error): #(5) for characteristic in self.service.characteristics(): if characteristic.properties() == 18: peripheral.readValueForCharacteristic_(characteristic) break def peripheral_didWriteValueForCharacteristic_error_(self, peripheral, characteristic, error): print 'In error handler' print 'ERROR:' + repr(error) def peripheral_didUpdateNotificationStateForCharacteristic_error_(self, peripheral, characteristic, error): print "Notification handler" def peripheral_didUpdateValueForCharacteristic_error_(self, peripheral, characteristic, error):#(6) print repr(characteristic.value().bytes().tobytes()) value = characteristic.value().bytes().tobytes() temp = decode_value(value[1:3],0.01) print 'temprature:' + str(temp) humid = decode_value(value[3:5],0.01) print 'humidity:' + str(humid) lum = decode_value(value[5:7]) print 'lumix:' + str(lum) uvi = decode_value(value[9:7], 0.01) print 'UV index:' + str(uvi) atom = decode_value(value[9:11], 0.1) print 'Atom:' + str(atom) noise = decode_value(value[11:13], 0.01) print 'Noise:' + str(noise) disco = decode_value(value[13:15], 0.01) print 'Disco:' + str(disco) heat = decode_value(value[15:17], 0.01) print 'Heat:' + str(heat) batt = decode_value(value[17:19],0.001) print 'Battery:' + str(batt)
- (1) アプリケーション起動後、Bluetoothモジュールの初期化が完了したらペリフェラルのアドバタイズをスキャンするモードになります。
- (2) ペリフェラルが見つかったらUUIDと比較を行い、目的のペリフェラルか調べます。接続処理とスキャンを停止しています。
- (3) ペリフェラルとの接続が呼ばれたらこのメソッドが呼ばれます。続いてサービスの検索を実行しています。
- (4) サービスが見つかったら続けて、キャラクタリスティックの検索を行います。
- (5) 検索したキャラクタリスティックが見つかったらデータの取得要求を行います。
- (6) データを受信したら呼ばれるメソッドです。データは必ずバイト列で送られてくるので、データを分解し各センサの値に変換します。
以下のメソッドでWxBeacon2のデータをデコードします。
#Decoding sensor value from Wx2Beancon Data format. def decode_value(value, multi=1.0): if(len(value) != 2): return None lsb,msb = struct.unpack('BB',value) result = ((msb << 8) + lsb) * multi return result
まとめ
今回はCoreBluetoothをPythonから呼び出し、BLEからデータを読み出してみました。CoreBluetooth自体がわかりやすいAPIになっているため、比較的に簡単に実装することができました。 BLEの基礎やCoreBluetoothの詳細については以下の書籍をお勧めします。今回の実装でも一部参考にさせていただきました。
- 作者: 堤修一,松村礼央
- 出版社/メーカー: ソシム
- 発売日: 2015/03/23
- メディア: 単行本
- この商品を含むブログを見る
その他参考
WxBeaconのデータのパース方法
PyObjCとCoreBluetooth
PythonからBLEを制御するライブラリの調査
はじめに
この記事ではPythonのBLE制御ライブラリに調査を行った結果をまとめています。2017年のMaker Fair Tokyoのウェザーニュースブースにて、WxBeacon2というBLEの環境センサを購入しました。このデバイスはOMRONの2JCIE-BL01まんまの代物です。本家は加速度センサが入っているようですが、ブースの方曰くこちらは入っていないかもとのこと。
しばらくはiPhoneから接続して専用アプリで遊んでみました。せっかくのなので、自分でアプリを作って遊んでみることにしました。今回はRaspberry Pi Zero WとMac OSX上で動作するアプリケーションを作成してみようと考えてみます。実装に利用する言語はPythonを選択しました。PythonのBLEライブラリを検索するといくつか種類が出てきたので、目に付いたものを調査してみました。
Python用BLEライブラリの現状
Python用のBLEライブラリを各プロジェクトのページから調べた結果は以下の表の通りです。4ライブラリを調査しました。
ライブラリ名 | ライセンス | 対応OS | Python | 機能 | 備考 |
---|---|---|---|---|---|
pybluez | GPL-v2 | Linux, Mac, Windows |
2.7系,3系 | セントラルの機能 | El Capitan or Sierraで動作し無さそう |
bluepy | GPL-v2 | Linux | 2.7系 3.3 3.4系 | セントラルの機能 | |
pyGATT | Apache2.0 MITライセンス |
Linux, Windows | 2.7系 | BGAPIのラッパー(Windows, Linuxのみ) bluezのgatttoolラッパ(Linuxのみ) |
WindowsではBLEが使えない? |
lightblue | GPL v3 | Linux, Mac |
2.7系,3系 | セントラルの動作のみ | メンテされていない。 OSXに関しては10.4時代で更新停止している。 |
BluefruitLE | MITライセンス | Linux(Bluez), Mac(CoreBluetooth) |
2.7系 | BLEのセントラルの動作のみ |
以下に各ライブラリに対する所感を記載します。
pybluez
一番使われていそうなpybluezですが、その名の通り、Linuxの標準BluetoothスタックのBluezのラッパのようです。Macにインストールした場合はlightblueと言う別のライブラリ経由でOSXのCoreBluetoothライブラリを利用しています。ですがEl CapitanやSierraではインストールに失敗します。リポジトリ上は修正されているようですが、そもそも依存しているlightblueがOSX10.4時代から更新されていないようなので使えないでしょう。またBluezとリンクして動くため、GPLライセンスとなってしまいます。ホビーの場合問題ないですが、個人的に好きではありません。
bluepy
BluezをベースにしたGPL-v2ライセンス。Linuxのみ対応しているがPython3系にも対応している。2017年9月現在もリポジトリに変化あり。開発自体は継続中の模様。
pyGATT
次にpyGATTはOSX非対応のため、要件から外れます。Bluezのgatttoolコマンドのラッパのようです。WindowではBGAPIというBluetoothシリアルのAPIを提供するようです。特定メーカのドングルでないとダメそう。
lightblue
lightblueはpyblueのMac対応に使われているライブラリです。が、メンテされておらず、前述の通り最新のOSXには対応していません。
BluefruitLE
最後に残ったBluefruitLEですが、更新もされており、上記の中では一番筋がいいように思えます。Windowsも動けば尚良しですが、今回の要件からは外れるので問題ありません。
まとめ
PythonでBLEを制御するライブラリをまとめてみました。どのライブラリもBLEの制御はOS依存のライブラリをラップしているため、すべてのプラットフォームで共通するライブラリは難しそうです。またLinuxのBluez自体がGPLライセンスのため、コピーレフトに敏感な場合は注意が必要そうです。Windows環境を無視すればBluefruitLEが一番筋が良さそう?Macについてはpyobjc経由でCoreBluetoothを叩いているようでした。自分でもpyobjcからBLE制御してみましたが、あっさりと動いたので、個別のOSごとに対応するのは簡単そうです。同じコードで動かすのは骨が折れそうです。
RestTemplateでトークン認証なAPIにアクセスした時のトークン有効期限切れに対応してみた。
この記事の概要
この記事はSpringBoot 1.5.6でRestTemplateを使い、認証トークンの有効期限切れに対応した際の記録を紹介しています。SpringBootとSpringを熟知しているわけではないので内容に多分の誤解がふくまれているかもしれません。 トークン認証が必要なAPIにアクセスする場合に、事前に何らかの方法で認証トークンを取得し、リクエストヘッダにトークンをセットしてAPIにアクセスするの方法が一般的です。また、多くのAPIで認証トークンの漏洩などによる不正利用に対応するため、認証トークンには有効期限が設けられています。 そのため、RestTemplateでAPIにアクセスする直前に都度認証トークンを取得する必要があります。しかし、APIサーバへのリクエストが遅い場合(ネットワーク的に遠くにあるなど)2回アクセスに時間がかかりパフォーマンスを落としてしまいます。そこで、トークンをキャッシュに保存しておき、使い回す方法が考えられます。 しかし、この場合、キャッシュに保存された有効期限を考慮しなければ、期限切れのトークンでアクセスし、リクエストが受け付けられない事態が発生します。 この記事ではトークンの有効期限を考慮せずに、キャッシュに保存したトークンを使い回すため、アクセス時に認証エラーが返ってきた場合に再度トークンを取得し、リクエストをリトライする方法を紹介します。
キャッシュ事態に有効期限を設ける方法も考えられますが、Spring Cacheの利用を考えたばい、キャッシュに有効期限を設定できるかはキャッシュに利用する製品に依存します。(Redisを利用する場合はできるようです)
以下の3つを順を追って説明します。
1.認証トークンの取得とセット
2.認証トークンのキャッシュ化
認証トークンの取得とセット
そもそもリクエストへの認証トークンの設定はRestTemplateのClientHttpRequestInterceptorインタフェースを実装したInterceptorの中で行います。実際のTokenの取得処理はTokenManagerクラスを作り、そちらで取得を行います。
- ClientHttpRequestInterceptorを実装したRequestTokenInterceptor(ソース1)
@Component public class RequestTokenInterceptor implements ClientHttpRequestInterceptor { private BTokenManager tokenManager; public TokenInterceptor(TokenManager tokenManager){ this.tokenManager = tokenManager; } @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { request.getHeaders().add("Authorization", "Bearer "+tokenManager.getAuthenticationToken());//(1) ClientHttpResponse response = null; response = execution.execute(request, body); return response; } }
(1)リクエストの ヘッダにTokenManagerから取得したトークンをBearerトークンとして設定する。
また、この時、TokenManagerはAPIに対して何らかの認証処理を行い、API Tokenを取得しています。
- TokenManager(ソース2)
@Component public class TokenManager { ; @Value("${bing.auth.url:}") private String authUrl; private RestTemplate restTemplate; public TokenManager(RestTemplateBuilder restTemplateBuilder, BasicAuthRequestInterceptor interceptors){ restTemplate = restTemplateBuilder.additionalInterceptors(interceptors).build();//(1) } public String getAuthenticationToken(){ URI uri = UriComponentsBuilder.fromUriString(authUrl).build().toUri(); String result = restTemplate.postForObject(uri, null, String.class);//(2) return result; } }
(1) BasicAuthのためのヘッダを設定するInterceptorを認証処理用のRestTemplateに設定する。今回はToken取得処理の認証情報の設定もInterceptorを利用。 (2) getAuthenticationTokenメソッドが実行されると認証URIにPOSTリクエストを発行し、トークンを取得してからStringとして返す。
この方法ですと、APIにアクセスするたびに、認証リクエストが発行され、1回余分にリクエストが実行されることになります。
認証トークンのキャッシュ化
そこで、Spring Cacheの機能を使い、認証トークン取得メソッドの結果をキャッシュし、2回目以降のリクエストはキャッシュを利用するようにします。変更点はTokenManagerのみで、以下のように getAuthenticationTokenメソッドに@Cachableアノテーションを設定します。Spring Cacheを有効にするため、別途クラスの先頭に@CacheEnableを付与する必要があります。
- トークン取得処理のメソッドのキャッシュ対応(ソース3)
@Cachable("authToken") public String getAuthenticationToken(){ URI uri = UriComponentsBuilder.fromUriString(authUrl).build().toUri(); String result = restTemplate.postForObject(uri, null, String.class);//(2) return result; }
これで、2回目のアクセス以降はキャッシュからトークンが返され、認証リクエストは実行されなくなります。ですが前述の通り、キャッシュに登録されているトークンの有効期限が切れた場合にAPIサーバからエラーで弾き返されることになります。APIサーバの使用にもよりますが、多くの場合401や403が帰ってくるかと思います。
トークン有効期限切れのリクエストリトライ
続いて、本題のリクエストリトライの方法です。まずTokenManagerにキャッシュをクリアするメソッドを追加します。以下のメソッドをTokenManagerに追加します。 特に処理はないですが、@CacheEvictアノテーションで指定したキャッシュが削除されます。
- キャッシュクリアするメソッド(ソース4)
@CacheEvict(value="authToken") public void clearCacheTokenInfo() { log.info("Rfresh token cache."); }
APIへのリクエストが実行された時、RequestTokenInterceptor::interceptメソッドの中で40xのエラーを検知して、キャッシュをクリアし、新しく Tokenを取得して再度、リクエストを投げます。具体的には上記ソース1を以下のソース5のように修正します。
@Component public class RequestTokenInterceptor implements ClientHttpRequestInterceptor { private BTokenManager tokenManager; public TokenInterceptor(TokenManager tokenManager){ this.tokenManager = tokenManager; } @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { request.getHeaders().add("Authorization", "Bearer "+tokenManager.getAuthenticationToken()); ClientHttpResponse response = null; response = execution.execute(request, body); if(response.getStatusCode()==HttpStatus.FORBIDDEN || response.getStatusCode()==HttpStatus.UNAUTHORIZED){//(1) tokenManager.clearCacheTokenInfo();//(2) List<String> token = new ArrayList<>(); token.add("Bearer "+tokenManager.getAuthenticationToken()); request.getHeaders().put("Authorization", token);//(3) response = execution.execute(request, body);(4) } return response; } }
(1) responseのステータスコードを確認して、FORBIDDENまたはUNAUTHORIZEDが帰って来ればリクエストリトライの処理を実行する。トークンエクスパイアの際の実際の挙動については各APIの仕様を調べる必要がある。 (2) リクエストリトライの処理の最初に現在のキャッシュをクリアする。 (3) 今回はAUthorizationヘッダを上書きするため、HttpHeaders::addではなく、HttpHeaders::putを使い値を上書きする。また、この時、TokenManager::getAuthenticationTokenを実行することで、再度トークンがキャッシュされる。 (4) リクエストを実行し、新しい認証トークンを使いAPIにアクセスする。
あとはAPIにアクセスするためのRestTemplateにRestTemplateBuilder経由でinterceptorを追加すれば、トークンの有効期限切れを機にすることなく、透過的にAPIにアクセスすることができます。
まとめ
そもそも認証トークンを一定期間キャッシュで保持するのは一般的なのか知りたいところです。今回の方法ではトークンの有効期限切れを避けるため、キャッシュの残存時間の設定を調整する必要がないのがミソでしょうか。
RestTemplateでレスポンスヘッダ内のContent-Typeを変更する。
この記事で紹介すること
この記事ではSpringのRestTemplateで受け取ったレスポンスのContent-Typeを変更し、任意のHttpMessageConverterを実行させる方法を考えたのでメモとして記載します。この記事はQiitaに投稿した記事をサンプルとして転載しています。
外部のAPIサーバを利用した際に、JSONフォーマットのbodyにContent-Type: text/plainで帰ってくることがありました。レスポンスのJSONに対応したBeanに値をバインドさせようとしてもContent-Typeがtext/plainのため、期待したHttpMessageConverterにマッピングされません。 そこで、RestTemplateにレスポンスのヘッダを変更するClientHttpRequestInterceptorを登録し解決しました。
ClientHttpRequestInterceptorの実装とRestTemplateBuilderの利用方法は以下のサイトを参考にしました。
またBing Speech API を対象に今回の内容を実装したものを以下のリポジトリに置いています。認証などの要素も入っているため、今回の説明とは多少異なります。 https://github.com/masato-ka/speech-api-proxy-service
ClientHttpRequestInterceptorの実装
- ResponseHeaderInterceptor.java
@Component public class ResponseHeaderInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { ClientHttpResponse response = execution.execute(request, body);//(1) response.getHeaders().setContentType(MediaType.APPLICATION_JSON);//(2) return response; } }
(1)実際のリクエスト実行処理
(2)リクエスト実行後、レスポンスオブジェクトのContentTypeをapplication/jsonで上書きします。
RestTemaplateの設定と実行
- RestClient.java
@Component public class RestClient { private final String apiUri = "https://hoge.com/api/v1/foo" private final RestTemplate restTemplate; public RestClient(RestTemplateBuilder restTemplateBuilder, ResponseHeaderInterceptor interceptor){//(1) restTemplate = restTemplateBuilder.additionalInterceptors(interceptor).build();//(2) } public Result request(){ URI targetUri = UriComponentsBuilder .fromUriString(apiUri) .build() .toUri(); Result result = restTemplate.getForObject(targetUri, Result.class);//(3) return result; } }
(1) ResponseHeaderInterceptorをInjection
(2) RestTemplateBuilderを通してResponseHeaderInterceptorをRestTemaplateに設定
(3) リクエストを実行、この際にResponseHeaderInterceptorの処理がフックされる
まとめ
そもそもサービス側がContent-Typeを正しく返してくれればいいのですが、コントロールできない他者のサービスだったため、このような方法をとりました。 そもそもInterceptorを実装しない簡単な方法があるかもしれません。
Konashiを使ってみた
ユカイ工学さんのKonashiで遊んでみた。
http://konashi.ux-xu.com/
はじめてのObjective-Cつらい。。。グラフ書くのにCore-Plotというライブラリを利用したんだけでも、
ただ折れ線グラフ書くだけで心が折れそうに。
説明:
iOSシミュレータでKonashiに接続後、iOS側からデジタルIOを制御してます。
その後圧力センサの値をAD変換で取り込んでグラフに出力しています。
何作ろうかな。
detectFace APIのPythonサンプル
FaceBookに買収されたFACE.comが開発者向けAPIを停止するとのことなので、他の類似サービスがないか探してみました。そうすると、日本の会社がhttp://detectface.com/:detectFace();という類似サービスを公開しているようです。こちらはアプリケーションキーの登録などは必要なく、POSTやGETで画像データを送るだけで利用できます。
今回はPythonでローカルに置いている画像をPOSTするクラスを作ってみました。
#!/usr/bin/env python # -*- coding: utf-8 -*- ''' Created on 2012/07/15 @author: masato-ka ''' import os import urllib2 import urllib class detectFaceProxy(object): def __init__(self,mode=0,force=0): self.detect_face_url='http://detectface.com/api/detect?m={m}&f={f}'.format(m=mode,f=force) self.data='' def fetch_feature(self,image_file_path): """ detectFaceAPIに画像を投げ込み、特徴点XMLを返す。 :param String image_file_path: 顔画像のファイルパス :rtype:String :return: 結果xml """ path,type=os.path.splitext(image_file_path) content_type=type[1:] if content_type=="jpg": content_type="jpeg" data = self._read_image_binary(image_file_path) request = urllib2.Request(self.detect_face_url) request.add_header('Content-Type','image/{t}'.format(t=content_type)) request.add_data(data) opener = urllib2.build_opener() self.data = opener.open(request).read() return self.data def _read_image_binary(self,file_path): return open(file_path,"r").read()
detectFaceProxyクラスのインスタンスからfetch_featureメソッドを実行してください。fetch_featureメソッドには画像ファイルのパスを渡します。返値として結果xmlがかえってきます。また、detectFaceProxy.dataにも同じものが格納されます。結果のxmlはxml.dom.minidomなんかで適当にパースすると扱いやすくなります。
単純に顔と目の位置、特徴点の位置と信頼度を返すだけですので、FACE.comのように性別の判定などは行っていないようです。
特徴点をJubatusなんかに食わせられるだろうか。
PythonでJubatusのregressionクライアントのサンプルコードを作ってみた。
Jubatusnoのregressionのサンプルコードを作ってみました。基本的には公式サンプルのclassifierをベースに書いてるので、対比して読みやすいかと思います。例によってPythonです。
regression(回帰)
regression(回帰)とは独立変数と従属変数のデータセットからその関係性を求めます。未知の値の独立変数を入力したときに、その独立変数に対する従属変数の値を推定できます。
簡単に説明すると、最小二乗法を考えるといいのですが、X(独立変数)とY(従属変数)のデータの集まりから、Y=b+aXのa とbを求めます。こうすることで、未知のXが来た際もYの値を推定することができます。もちろん実際は独立変数がX1 X2 X3....と複数存在したり、推定すべき式の形(モデル)もn次関数であったり、複雑です。また、これらを求める手法としては前述の最小二乗法だけでなくベイズ理論を用いた方法などが存在します。JubatusではPAと呼ばれるアルゴリズムを用いたPA-regressionというものを用いています。
回帰ができると、株価の予想や、気温と消費電力の関係、市場予測など様々な分野に応用できそうですね。センサーの値と事象を学習させて未知の事象をセンサーが観測してもそれに対する正しい出力を予想できそうです。
サンプルコード解説
細かいことはどうでもいいのでサンプルコードは以下のようになっています。
#!/usr/bin/env python # -*- coding: utf-8 -*- import sys from jubatus.regression import client from jubatus.regression import types def parse_args(): from optparse import OptionParser, OptionValueError p = OptionParser() p.add_option('-s', '--server_ip', action='store', dest='server_ip', type='string', default='127.0.0.1') p.add_option('-p', '--server_port', action='store', dest='server_port', type='int', default='9199') p.add_option('-n', '--name', action='store', dest='name', type='string', default='tutorial3') p.add_option('-a', '--algo', action='store', dest='algo', type='string', default="PA") return p.parse_args() if __name__ == '__main__': options, remainder = parse_args() regression = client.regression(options.server_ip,options.server_port) str_fil_types = {} str_fil_rules = [] num_fil_types = {} num_fil_rules = [] str_type= {} str_rules = [] num_type = {} num_rules = [types.num_rule("value","num")] converter = types.converter_config(str_fil_types, str_fil_rules, num_fil_types, num_fil_rules, str_type, str_rules, num_type, num_rules) config = types.config_data(options.algo, converter); pname = options.name regression.set_config(pname,config) print regression.get_config(pname) print regression.get_status(pname) with open(".//train.csv") as f: rows = f.read().split("\r") power_list = [row.split(",") for row in rows] for p in power_list: print p train_data=[(float(list[0].replace(":",".")),types.datum([],[["value", float(list[1])]])) for list in power_list] for t in train_data: regression.train(pname,[t]) regression.get_status(pname) regression.save(pname, "tutorial3") regression.load(pname, "tutorial3") regression.set_config(pname, config) regression.get_config(pname) with open(".//test.csv") as f: rows = f.read().split("\r") power_list = [row.split(",") for row in rows] for p in power_list: datum = types.datum( [], [["value",float(p[1])]] ) ans = regression.estimate(pname,[(datum)]) print "estimate:%s actual:%s" %(ans[0],p[0])
以下かいつまんで説明します。
import モジュール
次のモジュールをインポートします。
from jubatus.regression import client from jubatus.regression import types
regressionのクライアントインスタンス
regression = client.regression(options.server_ip,options.server_port)
classifierとは違って今回はregressionです。お間違いのないよう。引数はclassifierのサンプルと同じになっています。
フィルタの記述
str_fil_types = {} str_fil_rules = [] num_fil_types = {} num_fil_rules = [] str_type= {} str_rules = [] num_type = {} num_rules = [types.num_rule("value","num")]
filterの記述方法についてはJubatusの公式ページにリファレンスが公開されているので、詳しくはそちらを参照いただければと思います。今回は読み込ませるデータが数値だったため、num_rulesのみ追加しています。valueの値をそのまま使うという意味です。numのほかにも値の対数値を使うlogという指定があります。フィルターは利用しません。
テストデータの読み込み
with open(".//train.csv") as f: rows = f.read().split("\r") power_list = [row.split(",") for row in rows] for p in power_list: print p
Jubatusと関係ないところですが、trac.csvというファイルからcsv形式のデータをリスト形式で読み込んでいます。今回使用したデータは[時間,東京の消費電力]というものです。元データは東京電力の電気予報のサイトから入手できます。http://www.tepco.co.jp/forecast/html/images/juyo-2011.csv:東京電力公開2011年の消費電力
このデータからヘッダ情報、年月日を取り除き、時間と消費電力のデータのみを利用しています。
データの作成と学習
train_data=[(float(list[0].replace(":",".")),types.datum([],[["value", float(list[1])]])) for list in power_list] for t in train_data: regression.train(pname,[t]) regression.get_status(pname)
train_dataが(ラベル,datumオブジェクトのリスト)です。datumオブジェクトは次のように定義しています。[ [], [ ["value",float型の数値] ] ]
Jubatusでは学習データや結果をdatumというオブジェクトで表現しています。datumは以下の用になっています。
( [ ["user/id", "ippy"], ["user/name", "Loren Ipsum"], ["message", "<H>Hello World</H>"] ], [ ["user/age", 29] , ["user/income", 100000] ] )
データはkeyとvalueのペアになっています。このkeyとvalueはリストの要素になっており、[key,value]という形で表します。また、前半のリストはvalueが文字列のもの。後半のリストはvalueが数値になっているものです。datum[ svkey,value, nv[key,value]]という形になっています。
まとめると、[(label,datum),(label,datum)....]という形式のデータをregression.train()に与えています。なおregressionではlabelもdatumで与える数値もfloat型でないとだめなようです。ちなみにlabelが従属変数、datumの中の値が独立変数となります。
推定
with open(".//test.csv") as f: rows = f.read().split("\r") power_list = [row.split(",") for row in rows] for p in power_list: datum = types.datum( [], [["value",float(p[1])]] ) ans = regression.estimate(pname,[(datum)]) print "estimate:%s actual:%s" %(ans[0],p[0])
テストデータを読み込んで未知データを与えてみます。結果では推定値と実際の値を表示させます。ちなみに、電力と時間の関係では期待通りの結果を得ることができませんでした。消費電力と時間ではあまり相関がないと思うので当たり前かもしれませんが。気温と時間と消費電力だとかわってくるかもしれませんね。
まとめ
regressionは基本的にclassifierとそんなに違いがないので取っ付きやすいかもしれません。また、Jubatusのリファレンスもしっかりしてるので困らないと思います。
かなりおおざっぱで汚いコードですが、ご参考にしていただければ幸いです。もし、わかりづらい箇所があればコメントなどを残していただくとできる限り対応したいと思います。regressionできると応用範囲がさらに広がりそう!