TECH PLAY

アプトポッド

アプトポッド の技術ブログ

252

aptpod Advent Calendar 2019 11日目 先日 お菓子のデモの記事 を投稿したキシダがまたお送りします。みなさま、ここ最近『機械学習』とか『AI』とか耳にすることが多くなってきていると思いますが、現実はどれくらいの導入率かご存知ですか? なんと、 14〜15% (※1)らしいです。 意外に導入まで成功しているプロジェクトはまだ増えてきていません。 そこで、『機械学習プロジェクトって具体的にどういうふうにすすめるの?』とか『普通のシステム開発と違って何が難しいの?』という疑問の声に勝手にお答えして、完全な独断と偏見ですが、一般的な機械学習案件に対してよくある困った事例のご紹介とそれに対して私個人が意識していることをこちらにまとめてみようかと思います。 技術的なところではなく、案件における考え方や進め方的なところを中心に掘り下げてます。 ※この記事で出てくる事例は架空のものです。 ※1 総務省・ICR・JCER「AI・IoTの取組みに関する調査 より引用 そもそも機械学習プロジェクトってどんなもの? 『AI的なやつが自動判定していろいろ運用が自動化するやつでしょ??』 うーん、はい、まあそんな感じです(雑) wikipediaさんにも聞いて見ましょう。 機械学習(きかいがくしゅう、(英: Machine learning、略称: ML)は、明示的な指示を用いることなく、その代わりにパターンと推論に依存して、特定の課題を効率的に実行するためにコンピュータシステムが使用するアルゴリズムおよび統計モデルの科学研究である。 うーん、さすがwiki先生、頭がよすぎて凡人には全然わからないです。 私の理解では、機械学習はAI(人間並みの知能をコンピューターで実現するもの)のレベルではなく、あくまで「大量のデータを繰り返しコンピューターが計算し、パターンを見つけること」が機械学習という理解です。 このパターンの算出をアルゴリズムとして定義し、あるデータを入力値として、分類などの算出結果を出力するツールとなります。これを使用してお客様の課題を解決することが、いわゆる『機械学習プロジェクト』を指すと思っています。 もう少し掘り下げると、機械学習は以下の3種類があるそうです。 教師あり学習 教師なし学習 強化学習 私は『教師あり学習』の案件のみ経験があるので、本記事では『教師あり学習』にフォーカスした考察を記載します。   ちなみに、機械学習自体の説明については以下のQiitaがとてもわかりやすかったので、機械学習をザクッと知りたい方はこちらをご参考にどうぞ。 qiita.com そもそも機械学習案件ってどういう流れですすめるの? 機械学習のシステム開発は主に、以下の流れですすめることが多いです。 機械学習のシステム開発の一般的なフロー 上記の流れをぱっとみると、データの前処理とモデルの開発を除けば普通のシステム開発と一緒だな、って感じがしますよね。ただ、機械学習案件では人間がすべて明確なアルゴリズムを仕様として決めているわけではないので、品質の不確実性が必ずつきまといます。それにより案件をすすめることがどれだけ大変になるか、それぞれにフォーカスしてみましょう。今回はすべて触れると長くなってしまうので、前半の3つをメインにフォーカスしてすすめたいと思います。 CASE 1:企画フェーズにありがちなこと 企画フェーズでは、実際にお客さん側に何かしら課題があって、AIを使ってこの課題を解決しよう!という流れが発端で企画されることがよくあります。その際は、以下のことを考えてすすめるケースがよくありますね。 AIを導入することでビジネス的にどんなインパクトがあるか AIが何%くらいの正答率だったらビジネス的にうまくいくか 上記をみるだけだと、なんとなくすすめられそうです。 しかし、 お客様の上司『予算余ったし、ためしに今有り余ってるデータをAI使っていろいろ利用できるんじゃない?』 この上司の一言をそのままにしてすすめてしまうと、悪夢を呼び寄せてしまいます。 AI屋さん『データをつかって試しに簡単なモデルを作ったら、85%くらいの精度がでました!』 お客様『お、いいね!! だったら90%くらいの精度を目指して、モデル作ってみてよ!(これで上司になにか報告できそうだ!)』 AI屋さん『はい!!』 その後、AIベンダーは特定のデータに対して、ひたすら精度の高い90%のモデルを作り出し、早速そのモデルを使って運用することになりました。 お客様『あれ、90%の精度でも、実際に運用に乗せると意外に間違いが目立つよ?』 お客様『こんなに間違いが出ると修正が大変だよ。』 お客様『これ人間が手動でやってたほうが楽だったんじゃ。。。』 お客様『このモデル全然つかえないじゃん。。。どうゆうことだよ。。。』 AI屋さん『・・・・・(え、90%でいいって合意をとったはずなのに・・・。)』 お互い不幸になってしまいました。こういう例も少なからずあり、『なんとなく精度のいいモデルをつくってみてよ、データはあるからさ』から始まるプロジェクトでも、そのままなんとくですすめると『あれ、我々ってなにやりたいんだっけ』、『本当にモデルっているんだっけ』、となるわけです。 どうすればよいのでしょうか・・・。 考察:お客様のビジネスインパクトを評価できるKPIは事前に整合しておく 機械学習案件において、ここを握らなければお客さんとベンダー側のコミュケーションが成り立たないことがほとんどです。そもそもお客様の現状の運用における問題から、そこが改善できたと言えるKPIを予め共通認識としてもっておかなければ、AIつかってみたけどそこまで効果ないね、と残念なシステムができてしまいます。 私の場合は、以下の項目を明確化し、KPIを決めていくように心がけています。 お客様の問題点を明確化する (Why) どの指標を改善すれば問題点が収束するかを明確化する (What) その指標の改善方法に、機械学習を使用する以外に方法はないか検討する (How) 指標がきまってもその時点で終わりではなく、お客様を含めたプロジェクトの参画者はプロジェクトの間は延々を意識し、そのKPIにこだわり続ける責務があります。   また重要なのは、『整合したから大丈夫だよね!』ではなく、お客様周りの環境も必ず変わることを意識することです。そのためには、お互いの間で指標は常に可視化し、本当にその指標で正しいか定期的に議論することがベストです。指標がコロコロ変わることに戸惑うこともありますが、『変わること』より 『変わることが可視化できていないこと』 が問題だったりするので、開発側としてはアジャイル的な思想で柔軟に対応することも必要かもしれないですね。 CASE 2:データの前処理・精査にありがちなこと ここでは、学習を行うために必要な『教師データ』の作成を行うため、お客様が持っているデータについて精査していきます。 精査した後は、実際にお客様のデータに対してタグ付けを行い(これをアノテーションといいます)、教師データを作成していくフェーズとなります。 このフェーズでは主に以下のようなことを考えます。 学習できる量に相当するデータが揃っているか データが構造化されているかどうか セキュリティ的に問題があるかどうか データの加工にコストがどれだけかかるか アノテーションコストがどれだけかかるか やはり、壁だらけですね。 上記の詳細は割愛しますが、仮に上記をクリアしても以下のような落とし穴があるのです。 あるプロジェクトで、提供している商品に対するお客様からいただいたコメントに対して、分類を自動化するツールを開発している会社さんがありました。 お客さんから受け取ったメッセージを『もう一度使いたい!最高!などのプラス評価のメッセージ』、『クレーム、いらつきなどのマイナス評価のメッセージ』、『どちらでもない無評価メッセージ』に分けようと試みています。メッセージの分類をタグ付けとして実際に行うのは、実際にメッセージに対して対応するオペレーターです。 リーダー『データも構造化してデータ収集も自動的にできて、かつオペレーターが正解データを付与してくれる仕組みをつくったぞ!!』 リーダー『よし、ある程度の教師データも揃ったし、モデル作って精度試してみよう!』 しかし、モデルを作った結果、ある出来事が起きます。 リーダー『あれ、、、これはクレームと判定してほしいのに、プラス評価で判定されてしまっている!』 リーダー『あれ、教師データを見てみると、明らかにクレームなのにどちらでもない無評価になってるし、カテゴリ分けが正しくできてないじゃないか!どういうことだ!』 実際にアノテーションしたオペレーターに聞いてみると オペレーター A『僕はこれは褒めコメントだと思ってました』 オペレーター B『私はむしろ何がいいたいかよく分からなかったんでとりあえず無評価にしちゃいました』 リーダー『全然カテゴリ分けの判断基準が統一されていないじゃないか・・・!これじゃあモデルもちゃらんぽらんな判定になっちゃうよ。。。』 このケースは、データを分類する判断基準が曖昧で、正解データのタグ付けの基準がずれてしまい正しい教師データが作れないケースです。どんなにデータ収集の仕組みをシステム的に整えても、こちらを明確にしないと質の悪い教師データが作られてしまいます。 どうすればよいのでしょうか・・・。 考察:データの正解の条件を具体化し、事前に合意をとる このフェーズでは、お客様との期待値調整の一貫として、AIがどのレベルまでのデータを検知すればよいか明確にする必要があります。 そのためには、以下のようなことを意識すると良いかもしれません。 現状の運用方法を深く分析し、データのパターン化を行う パターンを決めるときは、できる限りその境界線を明確にする この境界線は一般的な事実を基準として考えるのではなく、プロジェクトのKPIからどう定義すべきか検討する データのパターンに対して、AIが担保するべきレベルを決める この際、企画フェーズで決めたKPIを元に、AIが提供すべきビジネスインパクトに基づき判断する AIが検出するべき 正解データ のパターンを、関係者全員が理解できるようにする ただこれは実際難易度が高く、データに詳しいクライアントがいないと実施が厳しいことが多いです。クライアント側でデータを一番良く見ている人、関わっている人は必ずプロジェクトに巻き込み、上記の議論を挟むようにすると良いかもしれません。 CASE 3:モデル開発・評価でありがちなこと 実際に企画内容も決まり、使用する教師データの作成も終了したら、いよいよモデルの作成です。 実際にモデルをトレーニングし、実際にできたモデルにたいして結果の正答率などを見てみます。 ここでよく陥りがちな事例とはなんでしょうか。 プロジェクトリーダー『絶対精度を90%にしたいぞ!』 AI屋さん『作成したモデルを評価したところ、今回用意したテストデータでは90%の正答率をクリアしました!』 プロジェクトリーダー『さすがじゃ!!これで大成功ですな!システム化しよう!』 実際システム化して新たなデータで検証したところ プロジェクトリーダー『あれ、全然90%の精度になってないじゃないか!どういうことだ!』 AI屋さん『90%の結果はあくまで評価時に使用したデータの話であって、新たに投入したデータが90%を常に超えるとは限らないです』 プロジェクトリーダー『そんな...!それじゃあ本番では全然つかえないじゃないか!』 AI屋さん『・・・(うーん、データも生き物だから、常に評価時と同じような精度がでるとは限らないんだけどな)』 モデルを作って評価したときはすごいうまく行っているようにみえたけど、本番で動かしてみると全然精度が出ないお話、これもよくあります。 モデルの評価時では、評価向けのデータセットを作る際に精度がでるようにデータセットをコントロールしたりすることは可能で、それ自体も1サンプルのデータなので参考値としては有効な数字です。そのため、1サンプルのデータがうまくいったからすべてのデータがうまくいく!ということはありません。モデルは必ず目標精度を担保できるものではなく、それを前提としてプロジェクトをすすめる必要があります。さて、困りました。 どうすればよいのでしょうか・・・。 考察1:モデルの精度検証は事前に整合したKPIで評価する モデルの評価では、基本お客様と事前に合意をとったKPIで評価し、お客様のビジネスインパクトがどの程度改善されるか計測した後、お客様に提示する必要があります。 最初にモデルの一般的な評価数値である 正答率 などをあげても、最終的にビジネスインパクトを満たす数値かどうか判定できないと意味がありません。 考察2:「がんばれば精度あがるでしょ!」から「精度が上がらないときどうしましょ」にシフトする 私自身、機械学習案件においては「予測誤りをなくすことはほぼ不可能」ということをプロジェクトメンバー全員が共通認識としてもっておく必要があるなと毎度強く感じます。 なぜかというと 精度はデータセットによって変化するため、特定のデータセットの評価値はあくまで参考値にしかならず、その評価結果をモデルが常に担保する数値ではないこと 追加学習を行えば、精度は必ずあがることはないということ 「いずれがんばりつづければ精度はあがるだろう」という空気感は、機械学習案件を泥沼化する考え方、と参考にしてた本やあらゆる有識者の話題にあがっていました。もしお客様にこの空気感があれば払拭しておく必要があり、以下のような精度が上がらなかったときの対応を事前に考え、共通認識として持っておく必要があります。   【精度が上がらない時の対応例】 ある程度人手を使いミスを補填する システムで事前に不正データを削減する プロジェクトを撤退する 3点目については、『え、諦めちゃうの・・・?』という方も多いと思いますが、KPIに対して撤退ラインは必ず設けておく必要があるそうです。機械学習案件は基本ギャンブルという人もいるほど、確実性を担保することが難しい案件でもあります。 まとめ いかがでしたでしょうか。今回は時間の都合上一部のフェーズしかご紹介できませんでしたが、上記以外にも機械学習案件にて難しい点は存在します。通常のシステムとは違い、テストにおける不確実性がどうしてもついてくるので、そのあたりをクライアントとどう握るかが鍵のようです。この点に関しては、弊社でも機械学習案件に関して議論をすすめ、模索しながらすすめているところです。もしこのような取り組みに興味がある方がいらっしゃいましたら、ぜひ以下の採用ページにアクセスをお願いします! 採用情報: https://www.aptpod.co.jp/recruit/ 一部以下の書籍・資料参考: - 5Dヒアリングシート(https://note.com/yukimimu/n/n90b997c6deef) - 仕事で始める機械学習 - O'Reilly Japan (https://www.oreilly.co.jp/books/9784873118215/)
この記事は Aptpod Advent Calendar 2019 の10日目の記事です。 先端技術調査グループの大久保です。 前回の記事 では、WebSocketのechoサーバにアクセスするwasmをRustとGoで作成しました。今回は、echoだけでは物足りないので、意味のあるバイナリデータをサーバから流して、クライアント側、すなわちWebブラウザ上に表示するまでやってみます。あまり大きくないデータならJSONにして文字列を流せば良いのですが、JSONだとサイズが問題になるようなケースを想定して、JSONよりコンパクトな CBOR を使ってみます。 ちなみに今回はRust作ったところで力尽きたので、対応するGoのコードは残念ながらありません。 実装 3つのクレートに実装を分けます。送るデータの定義、エンコード、デコードを担当するmydataクレート、サーバを担当するws-serverクレート、そしてクライアントを担当し、wasmにコンパイルされるws-clientクレートから成ります。前回同様、wasmにコンパイルされたRustからJavaScriptのAPIを使用するには wasm-bindgen を用います。データが全体でどのように流れるかは少し複雑ですが、以下の図のようになります。 mydataクレート Rustの構造体に格納したデータをシリアライズ/デシリアライズするために今回はcbor_serdeを用います。CBORはバイナリ版JSONみたいなフォーマットで、ちょうどいいRustの実装がありましたので使わせていただきます。シリアライザ/デシリアライザが用意できるならCBOR以外のどんなフォーマットでも同じように書くことができます。 serdeとcbor_serdeに依存するので、以下の依存関係をCargo.tomlに追記します。 [dependencies] serde = "1" serde_derive = "1" serde_cbor = "0.10" このクレートは他クレートから参照されるため、lib.rsにコードを書いていきます。 use serde :: Serialize; use serde_cbor :: de :: from_slice; use serde_cbor :: ser :: {IoWrite, Serializer}; use serde_derive :: {Deserialize, Serialize}; // 送りたいデータを定義する構造体 #[derive(Serialize, Deserialize, Debug )] pub struct MyData { pub time: u64 , pub data: Vec < u64 > , } impl MyData { // バイナリデータへエンコードし、Writeへ書き出す pub fn encode < W: std :: io :: Write > ( & self , w: W) -> Result < (), serde_cbor :: error :: Error > { let mut serializer = Serializer :: new ( IoWrite :: new (w)). packed_format (); self . serialize ( &mut serializer)?; Ok (()) } // スライスからMyDataへデコード pub fn decode (slice: & [ u8 ]) -> Result < MyData, serde_cbor :: error :: Error > { from_slice (slice) } } MyData構造体が実際に送りたいデータの本体で、UNIX時間とu64の配列とします。 #[derive(Serialize, Deserialize)] と指定することで、serde_cborのシリアライザ/デシリアライザでこの型が使えるようになります。これを利用してencode/decode関数を記述します。serdeの力によりエンコーダ/デコーダはかなり短く書くことができます。 ws-serverクレート 先ほど定義したMyDataを送信するサーバを作成します。以下の依存関係をCargo.tomlに追記します。 [dependencies] websocket = "0.23" rand = "0.7" mydata = { path="../mydata" } WebSocketを使うためにwebsocketクレートを、適当なデータ生成のためにrandクレートを、そして先ほど作成したmydataクレートを追加します。 main.rsは以下のようになります。 use mydata :: MyData; use std :: thread; use websocket :: sync :: Server; use websocket :: {Message, OwnedMessage}; // 送るデータのサイズを指定 const DATA_SIZE: usize = 512 ; fn main () { // サーバを立てる let server = Server :: bind ( "localhost:50000" ). unwrap (); for request in server. filter_map ( Result :: ok) { thread :: spawn ( || { let mut client = request. accept (). unwrap (); let ip = client. peer_addr (). unwrap (); println! ( "Connection from {}" , ip); let mut buf = Vec :: new (); for _ in 0 .. 100000 { let data = gen_random_data (DATA_SIZE); // MyDataを生成 data. encode ( &mut buf). expect ( "encode error" ); // バッファへエンコード let message = Message :: binary (buf. as_slice ()); client. send_message ( & message). unwrap (); // エンコードした結果を送る std :: thread :: sleep ( std :: time :: Duration :: from_millis ( 50 )); // 少し待つ buf. clear (); } let message = OwnedMessage :: Close ( None ); client. send_message ( & message). unwrap (); }); } } // 乱数を使ってMyDataを生成する fn gen_random_data (size: usize ) -> MyData { let mut data = Vec :: with_capacity (size); let time = std :: time :: SystemTime :: now () // timeにはUNIX時間を格納する . duration_since ( std :: time :: UNIX_EPOCH) . unwrap () . as_secs (); for _ in 0 ..size { data. push ( rand :: random ()); } MyData { time, data } } MyDataのtimeにはUNIX時間を、dataには適当な長さの乱数列を格納し、50msごとに送信します。 ws-clientクレート ws-clientでは、JavaScriptのAPIを使ってWebSocketのメッセージを受け取り、MyDataへデコードします。そして画面に表示する文字列を作成し、それをJavaScript側に渡します。ブラウザ上への反映はJavaScript側が行うものとします。JavaScriptへ渡す文字列は、サーバから送られてきたMyDataに格納されるUNIX時間と、dataの先頭にある乱数を表示するためのものです。 lib.rsは以下のようになります。 extern crate alloc ; use mydata :: MyData; use std :: cell :: RefCell; use wasm_bindgen :: prelude :: * ; #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = console)] pub fn log (s: &str ); } macro_rules! console_log { ( $($t :tt )* ) => ( log ( & format_args! ( $($t)* ). to_string ())) } #[global_allocator] static ALLOC: wee_alloc :: WeeAlloc = wee_alloc :: WeeAlloc :: INIT; use wasm_bindgen :: JsCast; use web_sys :: {ErrorEvent, MessageEvent, WebSocket}; #[wasm_bindgen(start)] pub fn start_websocket () -> Result < (), JsValue > { // WebSocketサーバに接続 let ws = WebSocket :: new ( "ws://localhost:50000" )?; // コールバックの登録 let onmessage_callback = Closure :: wrap ( Box :: new ( move | e: MessageEvent | { on_message (e); }) as Box < dyn FnMut (MessageEvent) > ); ws. set_onmessage ( Some (onmessage_callback. as_ref (). unchecked_ref ())); onmessage_callback. forget (); let onerror_callback = Closure :: wrap ( Box :: new ( move | e: ErrorEvent | { console_log! ( "error event: {:?}" , e); }) as Box < dyn FnMut (ErrorEvent) > ); ws. set_onerror ( Some (onerror_callback. as_ref (). unchecked_ref ())); onerror_callback. forget (); let onopen_callback = Closure :: wrap ( Box :: new ( move | _ | { console_log! ( "socket opened" ); }) as Box < dyn FnMut (JsValue) > ); ws. set_onopen ( Some (onopen_callback. as_ref (). unchecked_ref ())); onopen_callback. forget (); Ok (()) } // WebSocketのメッセージを受け取ったら呼ばれる関数 fn on_message (e: MessageEvent) { // WebSocketからBlobオブジェクトを受け取る let blob: web_sys :: Blob = e. data (). unchecked_into (); // Blobから&[u8]を取り出すために、データのロード後に呼ばれるon_data_loadedを登録する let promise = web_sys :: Response :: new_with_opt_blob ( Some ( & blob)) . unwrap () . array_buffer () . unwrap (); let callback = Closure :: wrap ( Box :: new ( move | array: JsValue | { on_data_loaded (array); }) as Box < dyn FnMut (JsValue) > ); promise. then ( & callback); callback. forget (); // 現状メモリリークしているが、もっといい書き方があるはず } thread_local! ( static CALLBACK: RefCell < Option < js_sys :: Function >> = RefCell :: new ( None )); // コールバックを設定するための関数 // この関数はJavaScriptから呼ばれる #[wasm_bindgen] pub fn set_callback (f: js_sys :: Function) { CALLBACK. with ( | callback | { * callback. borrow_mut () = Some (f); }); } // WebSocketから受け取ったデータを処理する関数 fn on_data_loaded (array: JsValue) { let array = js_sys :: Uint8Array :: new ( & array); let len = array. byte_length () as usize ; let mut buf: Vec < u8 > = vec! [ 0 ; len]; array. copy_to ( &mut buf); let data = MyData :: decode ( & buf). unwrap (); // CBORのデータをMyData型にする CALLBACK. with ( | callback | { if let Some ( ref callback) = * callback. borrow () { let data = format! ( "time : {} value : {}" , data.time, data.data[ 0 ]); // JavaScriptに渡す文字列 let data = JsValue :: from (data); // 文字列をJavaScriptに渡せるように変換する callback. call1 ( & JsValue :: NULL, & data). unwrap (); // 登録されたJavaScriptの関数を呼び出す } }); } 次のようなindex.htmlを用意します。 <!DOCTYPE html> < html > < head > < meta charset = "utf-8" > < title > Wasm + WebSocket Test </ title > </ head > < body > < noscript > This page contains webassembly and javascript content, please enable javascript in your browser. </ noscript > < script src = "./bootstrap.js" ></ script > < p id = "label" ></ p > </ body > </ html > おなじディレクトリにあるindex.jsの内容は次のようにします。 import * as ws_client from "ws_client" ; // 文字列を受け取って、HTMLの内容を更新する function on_data_loaded_callback(data) { var label = document .getElementById( "label" ); label.textContent = data; } // set_callbackはRust側で定義した関数 // ここにon_data_loaded_callbackを渡す ws_client.set_callback(on_data_loaded_callback); 実行結果 ws-server以下でサーバを立ち上げます。 cargo run ws-clientでは、wasmにビルドした後にnpmでWebサーバを立ち上げます。 wasm-pack build cd www npm start ブラウザで http://localhost:8080/ にアクセスすると、次の画像のようにUNIX時間となにかの乱数が表示されるはずです。サーバで生成された値を受け取っていることを確認できました。 ちなみに、今回のwasmファイルのサイズは187KB、最適化後は139KBでした。 thread_local を使うため #![no_std] を指定していないとはいえ、それなりのサイズになりました。 考察 WebAssemblyを使用する利点としてよくあげられるのは速度面です。そして、今回のように検討では受け取ったバイナリのエンコード部分をwasmにしています。これで高速になったり、CPU負荷が小さくなるのでしょうか。それはJavaScriptで同等の実装を作成して比較しなければわかりませんが、あまり今回の構成が優位ではない可能性があります。RustとJavaScriptをどうやってリンクさせているかは wasm-bindgenのマニュアル に書いてありますが、RustとJavaScript間で関数を呼び出しあったりオブジェクトを変換するのはそれなりにコストがかかります。今回のバイナリをデコードする程度であれば、JavaScriptで完結させた方が良い可能性もあります。 ただ、ブラウザ以外のネイティブアプリに載せるクライアントやサーバ側もRustで書くのであれば、送受信データのデコード、エンコード、データ定義等を共通化できる利点もあります。実際今回のコードでは、 MyData 構造体をサーバ/クライアント側両方で扱っています。wasmで実行する場合でも、もっと重い処理を挟むようならRustで書けば最適化できます。さらに将来的にはWASIを使うことで、JavaScriptのAPIに依存することなくwasmでアプリケーションが書ける可能性もあります。 今後の展望 今回の検討でWebAssembly周りのエコシステムについて色々調べましたが、Rustのサポートは妙に充実しています。今回使用したwasm-bindgenにより、かなりシームレスにRustとJavaScriptのやりとりができるようになっています。また、WASIを実装するwasmtimeはRustで書かれているようです。WebAssemblyに力を入れているMozillaがRustの元祖開発元であることもあり、今後WebAssembly周りでRustの利用が増えていくのではないかと個人的には予想しています。 まとめ Rustを使ってWebSocketによるサーバ→クライアント(Webブラウザ)の通信ができた JSONよりコンパクトなCBORを送ることができた。エンコード、デコードも簡単 WebAssembly関連でRustの利用が増えてくはず
「データが上がって来るの遅いけど、電波悪いからしょうがないな〜」 なんてアッサリ諦めてないでしょうか? そんな方に 「BBRを有効にすればスループットが上がるかも!」 という話を、 aptpod Advent Calendar 2019 の9日目ではお送りします。担当のサーバーサイドエンジニアに憧れるエンベデッドエンジニア ochiai です。 はじめに この記事では、BBRを有効にする方法と、スループットの実測にフォーカスしています。 なので、「BBRのアルゴリズムが知りたいんだよ!」とか、「輻輳制御アルゴリズムって何なのさ?」って方は、他の資料を参考にしてください。オススメを記載しておきます。 キーワード オススメ資料 BBRとは 本家Googleの資料 TCPの輻輳制御アルゴリズムの歴史 n月刊ラムダノート BBRって とはいっても、「いきなりBBRと言われても」と感じるかもしれませんので、簡単な紹介だけしたいと思います。 そもそも、電波が悪くてデータが届かない場合にはどうなっているのでしょうか? もちろん、届かなければ、もう一度送って届くようにして欲しいですよね。とは言っても闇雲に何度も送ったのでは、それはそれでネットワークに無駄な負荷がかかってしまいます。 このあたりを賢く制御してくれているのが、TCPの輻輳制御です。 そして輻輳制御アルゴリズムで今注目をあびているのがBBR(Bottleneck Bandwidth and Round-trip propagation time)になります。 BBRを有効にする では、BBRを動かすための準備をしていきましょう。 LinuxでBBRを使用するためには、カーネル4.9以降である必要があります。 ここでは、弊社で使用している Yocto での設定方法を紹介させて頂きます。 YoctoでBBRを有効にする カーネルコンフィグを変更する必要があるので、menuconfigで変更を行います。 Yoctoでmenuconfig をするには、下記のコマンドを実行します。 $ cd <Yoctoのpokyディレクトリ> $ source oe-init-build-env $ bitbake linux-yocto -c kernel_configme -f $ bitbake linux-yocto -c menuconfig 下図のようにGUIっぽい表示がされるので、方向キーと、スペースキーを使用して操作します。 BBRと、BBRで必要なFQを設定するためには、下記の箇所を変更します。 - Networking support - Networking options <=== Spaceキーで有効化 - TCP: Advanced Congestion Control - BBR TCP <=== * にする - Default TCP congestion control <=== BBRにする - QoS and/or fair queueing - Fair Queue <=== * にする 完了したらsaveしてmenuconfigを終了します。 出来上がった.configを defconfigとして登録 したら、いつも通りにYcotoのイメージを焼き上げて完了です。 BBRが使用されていることを確認する 作成したイメージをデバイスにインストールしたら、BBRが動作しているかを確認します。 確認は、sysctl、ss、tcコマンドで行えます。 変更前 # sysctl net.ipv4.tcp_available_congestion_control net.ipv4.tcp_available_congestion_control = cubic reno # sysctl net.ipv4.tcp_congestion_control net.ipv4.tcp_congestion_control = cubic # sysctl net.core.default_qdisc net.core.default_qdisc = pfifo_fast # ss -tni State Recv-Q Send-Q Local Address:Port Peer Address:Port ESTAB 0 280 [::ffff:10.0.1.11]:22 [::ffff:10.0.2.127]:61981 cubic wscale:5,7 rto:204 rtt:3.117/0.808 ato:64 mss:1448 rcvmss:1392 advmss:1448 cwnd:10 bytes_acked:4441 bytes_received:4249 segs_out:52 segs_in:87 data_segs_out:47 data_segs_in:40 send 37.2Mbps lastrcv:8 lastack:4 pacing_rate 74.3Mbps delivery_rate 10.8Mbps app_limited unacked:2 rcv_rtt:7 rcv_space:28960 minrtt:1.282 # tc qdisc show qdisc noqueue 0: dev lo root refcnt 2 qdisc mq 0: dev enp1s0 root qdisc pfifo_fast 0: dev enp1s0 parent :1 bands 3 priomap 1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1 変更後 # sysctl net.ipv4.tcp_available_congestion_control net.ipv4.tcp_available_congestion_control = cubic bbr reno # sysctl net.ipv4.tcp_congestion_control net.ipv4.tcp_congestion_control = bbr # sysctl net.core.default_qdisc net.core.default_qdisc = fq # ss -tni State Recv-Q Send-Q Local Address:Port Peer Address:Port ESTAB 0 280 [::ffff:10.0.1.11]:22 [::ffff:10.0.2.127]:61390 bbr wscale:5,7 rto:203 rtt:2.364/0.44 ato:40 mss:1448 rcvmss:1392 advmss:1448 cwnd:11 bytes_acked:4633 bytes_received:4205 segs_out:54 segs_in:88 data_segs_out:49 data_segs_in:39 bbr:(bw:14.0Mbps,mrtt:1.317,pacing_gain:2.88672,cwnd_gain:2.88672) send 53.9Mbps lastrcv:6 lastack:2 pacing_rate 41.7Mbps delivery_rate 14.0Mbps app_limited unacked:2 rcv_rtt:5 rcv_space:28960 minrtt:1.317 # tc qdisc show qdisc noqueue 0: dev lo root refcnt 2 qdisc mq 0: dev enp1s0 root qdisc fq 0: dev enp1s0 parent :1 limit 10000p flow_limit 100p buckets 1024 orphan_mask 1023 quantum 3028 initial_quantum 15140 low_rate_threshold 550Kbit refill_delay 40.0ms 測定 測定方法 測定は下記のパターンで行いました。 ネットワーク環境 LTE LTE(アッテネータ 10db 1 ) LTE(アッテネータ 20db) LAN LAN(パケットロス 1% 2 ) LAN(パケットロス 2%) LAN(パケットロス 4%) LAN(パケットロス 8%) 輻輳制御アルゴリズム BBR CUBIC(元々使用していたアルゴリズム) 送信データは、IoTデバイス内で動作する弊社の intdash Edge Module から、秒間一定量のデータを送信して計測を行いました。 測定結果 Network CUBIC [KByte/sec] BBR [KByte/sec] LTE (attenuation 0db) 674 631 LTE (attenuation 10db) 572 616 LTE (attenuation 20db) 261 559 LAN (loss 0%) 1126 1105 LAN (loss 1%) 1112 1138 LAN (loss 2%) 1078 1102 LAN (loss 4%) 779 1063 LAN (loss 8%) 381 1051 測定の結果、CUBICは、パケットロスする環境と、しない環境で、あきらかな差が出ました。 しかし、BBRは、パケットロスする環境と、しない環境で、あきらかな差が出ませんでした。 まとめ BBRとCUBIC(元々使用していたアルゴリズム)で、データのスループットを比較した結果、下記グラフのようになりました。 一般的に言われている通り、パケットロスが多い時ほど効果を発揮する 事がわかりました。 このことから、 通信環境が悪くパケットロスするような環境で、今までより早く送信できる ことが期待できます。 おわりに 最後まで読んでいただき、ありがとうございます。 はじめに書いた「BBRを有効にすればスループットが上がるかも!」が記事の趣旨の8割といっても過言ではないのですが、Yoctoで有効にする方法や、動作確認方法も参考になったようでしたら幸いです。 弊社のシステムでも、ここで紹介させて頂いた方法でBBRを有効にしたデバイスを順次出荷していく予定です! 以上、 aptpod Advent Calendar 2019 9日目担当の ochiai でした。 明日は、先端技術調査グループ期待の新星、 okubo さんです! アッテネータはLTEのアンテナに接続して信号を減衰させる装置 ↩ パケットロスはtcコマンドにより擬似的に発生させた ↩
はじめに こんにちは。 アドベントカレンダー 8日目担当、 サーバーサイドエンジニアの miyauchi です。 昨年は「 Goとクリーンアーキテクチャとトランザクションと 」と「 Vim初心者から中級者の入り口くらいまで 」を書きました。 早いものでもう一年経つのですね。 さて、弊社ではバックエンドシステムを従来のモノリス型のサービスから、徐々にマイクロサービスへ移行中です。 そんな中で分散トレーシング(とりわけOpenTracing)について調査する機会がありました。 よって、今年はGoのアプリケーションで、OpenTracingを使うときのコード集を、解説ありで書いていきたいと思います。 「OpenTracingとは!」や、「分散トレーシングとは!」のような概念部分は書きません。 また、OpenTracingのTracer実装である jaeger についても詳細は書きません。 OpenTracingを使ったサンプルコード中心で、 チュートリアルに毛が生えたくらいの入門記事となります。 OpenTracingの具体的な使い方については、案外まとまった記事がないのかなと思ったので書こうと思ったわけですが、 一方で、少し今更感もあります、、、が、張り切っていきましょう! 分散トレーシングについて 「概念部分は書かない!」とは言ったものの、いきなりコードはやっぱりしんどいかもしれません。 なので、分散トレーシングとOpenTracingについて少しだけ説明します。 サービスを細かく分割していくと、 一回のリクエスト(HTTPリクエストなど)で複数のサービスやミドルウェアをコールすることになります。 そうすると困ってくることの一つが、一つの処理を追跡することが難しくなることが挙げられます。 例えば 「このリクエストすごく重いんだけど、いっぱいサービスコールしていてどこがボトルネックなのかわからない!」 といったものですね。 分散トレーシングは分散システムにおいてのサービス間呼び出しの追跡を助けます。 そして、OpenTracingは分散トレーシングのための、仕様、ライブラリ、ドキュメントなどから構成される開発キットのようなものです。 詳細は公式Webサイトより。 https://opentracing.io/ それでは、早速ですがコードを見ていきます。 今回紹介するソースコードは全て aptpodのGithubリポジトリ にあります。 手元でコードを見たい方はクローンをしておきましょう。 以降、コマンドなどの実行は全て、同リポジトリルートがカレントディレクトリである前提で進めます。 ミニマムアプリケーションを実装する 実装アプリケーションについて 最初にクライアントからHTTPコールするだけの、すごく簡単なミニマムアプリケーションを実装していきます。 こんな感じです。 +--------+ | client | +---+----+ | ^ | | client | | side | +--------+--------- | server | | side | v | v +--------------+ | hoge-service | +---+----------+ はい、簡単ですね。 OpenTracingの実装の流れ 流れの説明をする前に、 OpenTracingのAPIを使っていると、 Tracer 、 Span 、 SpanContext という用語が頻出します。これらについて少しだけ補足します。 要所で簡単に解説はしますが、ちゃんと理解するために The OpenTracing Data Model 、 及び The OpenTracing API を読むことをおすすめします。 ここでは、 Span と Tracer についてだけ簡単に図と合わせて説明します。 -----------------------------------------------------> time | parentspan | +----------------------------------------------+ | child span | | child span | +------------+ +-----------------------+ | grand child span | +------------------+ 任意の時間に特定の期間である Span Span は入れ子にすることができる 一番最初に Span を開始するのが Tracer 概念だけふんわりイメージできればOKです。 繰り返しになりますが、正確な理解は本家ドキュメントを読むことをおすすめします。 では実際の実装の流れについて説明します。 OpenTracingを使うときは基本的には次のような流れになります。 Tracer の初期化(原則アプリケーション起動時に一回) Tracer の取得 Span の開始 親の Span から新しい Span の開始 親の Span から新しい Span の開始 Span の終了 ...(任意の Span の開始と終了) Span の終了 ...(任意の Span の開始と終了) Span の終了 Tracer から Span を開始して、 Span から Span を入れ子にしていくイメージです。 実装する Tracerを初期化する Span は Tracer が開始します。 まずはアプリケーションのどこからでも参照できる、 GlobalTracer に Tracer をセットしましょう。 Tracer はインターフェースとなっていて実装は様々です。 今回は、jaegerを使うので こちら のコードを参考にして、 Tracer を初期化します。 jaeger独自の設定部分の解説は割愛します。 詳細は 公式 や Githubのリポジトリ を参照してください。 下記のコードのいずれかをコールすると jaeger版の Tracer の初期化が完了し、 GlobalTracer にセットされます。 // ./lib/tracer.go package lib import ( "io" "github.com/opentracing/opentracing-go" "github.com/uber/jaeger-client-go" "github.com/uber/jaeger-client-go/config" "github.com/uber/jaeger-client-go/log" "github.com/uber/jaeger-lib/metrics" ) func InitGlobalTracer(serviceName string) (io.Closer, error) { return initGlobalTracer(config.Configuration{ Sampler: &config.SamplerConfig{ Type: jaeger.SamplerTypeConst, Param: 1, }, Reporter: &config.ReporterConfig{ LogSpans: true, }, }, serviceName) } func InitGlobalTracerProduction(serviceName string) (io.Closer, error) { return initGlobalTracer(config.Configuration{}, serviceName) } func initGlobalTracer(cfg config.Configuration, serviceName string) (io.Closer, error) { jLogger := log.StdLogger jMetricsFactory := metrics.NullFactory return cfg.InitGlobalTracer( serviceName, config.Logger(jLogger), config.Metrics(jMetricsFactory), ) } GlobalTracer にセットするコードを紹介しましたが、そうではなく、オブジェクトとして生成し使い回すことも可能です。 // ./lib/tracer.go func CreateTracer(serviceName string) (opentracing.Tracer, io.Closer, error) { var cfg config.Configuration jLogger := log.StdLogger jMetricsFactory := metrics.NullFactory cfg.ServiceName = serviceName return cfg.NewTracer( config.Logger(jLogger), config.Metrics(jMetricsFactory), ) } クライアントを実装する 次にクライアント側の実装コードを見ていきます。 // ./minclient/main.go package main import ( "errors" "io/ioutil" "log" "net/http" "github.com/aptpod/opentracing-sandbox/lib" "github.com/opentracing/opentracing-go" ) func main() { // Tracerの初期化 closer, err := lib.InitGlobalTracer("client") if err != nil { panic(err) } defer closer.Close() // Tracerの取得 tracer := opentracing.GlobalTracer() // Spanの開始 // このSpanを引き回す span := tracer.StartSpan("get_hoge") // Spanの終了 defer span.Finish() // Hogeの呼び出し! res, err := getHoge(span) if err != nil { panic(err) } log.Println(res) } このコード断片については難しいことはないと思います。「OpenTracingの実装の流れ」 でも述べた通り、 Tracer を初期化して... Tracer を取得して... Span を開始して... 処理(Hogeサービスの呼び出し)をして... Span を終了する。 という流れが読み取れると思います。 一点、 HTTPコールは単純にHTTPを呼び出せばよいというわけではありません。 サービス間の流れを追跡するOpenTracingですから、 Span の情報を引き継ぐ必要があります。 コード中でのHTTPコールの実装箇所は getHoge() 関数内にあたるので、 掘り下げて見ていきましょう。 // ./minclient/main.go ... omitted func getHoge(span opentracing.Span) (string, error) { // リクエストの生成 // 今回はHogeのPath url := "http://localhost:18080" req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return "", err } // ※ Tracerを使ってSpanの情報をInject if err := span.Tracer().Inject( span.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header), ); err != nil { return "", err } // 通常のHTTPアクセス resp, err := Do(req) if err != nil { return "", err } return string(resp), nil } サービス間で情報を引き継ぐには、HTTPリクエストに何かしらの方法で Span の情報を埋め込む必要があります。 引き継ぐ Span の情報( 情報 だと少し語弊があるかもしれませんが。。。) を SpanContext と言います。 OpenTracingでは Tracer が SpanContext をリクエストに埋め込む責務を追います。送信側でリクエストに SpanContext を入れることを Inject 、逆に受信側で SpanContext を取り出すことを Extract と言います。 また、 Inject して、 SpanContext を運ぶものを Career と言います。 今回はHTTPヘッダを Career として、 SpanContext を Inject しています。 さて、この Career ですが、HTTP以外のRPCコール時ももちろん考慮されていて、HTTPヘッダ以外にも種類があります。 気になる方はOpenTracingの実装を読んでみましょう。 // SpanContext伝播のイメージ +----------+ | service1 | `Inject` `SpanContext` to `Career` +---+------+ | | Carry using `Career` | v +----------+ | service2 | `Extract` `SpanContext` from `Career` +---+------+ さて、 Inject をしたら、あとは通常通りにHTTPコールすればOKです。 コード中では resp, err := Do(req) の部分ですが、この部分はそのままHTTPコールしているだけなので割愛します。 hogeサービスを実装する 今度は受信側の実装です。 package main import ( "log" "net/http" "time" "github.com/aptpod/opentracing-sandbox/lib" "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" ) func main() { // tracer の初期化 tracer, closer, err := lib.CreateTracer("hoge-service") if err != nil { panic(err) } defer closer.Close() log.Println("start hoge") http.ListenAndServe(":18080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("hoge") // SpanのExtract spanCtx, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header)) // リクエストからのSpanContextを引き継いで新しいSpanの開始 span := tracer.StartSpan("get_hoge", ext.RPCServerOption(spanCtx)) defer span.Finish() // ダミー処理 <-time.After(time.Second) w.WriteHeader(http.StatusOK) w.Write([]byte(`{ "name": "hoge!" }`)) })) } まずはTracerの初期化部分です。 サービス側の実装は GlobalTracer へセットするのではなく、新たに Tracer を生成してみます。 GlobalTracer でも良いのですが、サンプルなので。。。 1アプリケーションで複数の Tracer が必要な場合はそれぞれで生成するとよいでしょう。 次はリクエストハンドラ( http.HandlerFunc )の実装です。 リクエストハンドラ内で、まず伝播された SpanContext を Extract します。 そして、その SpanContext を利用して、新しく Span を開始しています。 ext.RPCServerOption(spanCtx) は SpanContext から適切な StartSpanOption を返す関数です。 そういうものだと思って指定します。 内容が気になる方は全然難しくない実装なのでぜひ追って見てください。 さて、ここまででミニマムコードができました。 動作確認 準備 Dockerで jaeger を起動しておきます。下記に示すコマンドで、UIも含めアプリケーションが起動します。 docker run --rm -p 6831:6831/udp -p 6832:6832/udp -p 16686:16686 jaegertracing/all-in-one:1.7 --log-level=debug ブラウザでUIを開いておきます。アドレスは http://localhost:16686/search です。 実行 hogeサービスを起動します。 go run ./minserver/*.go クライアントを実行します。 go run ./minclient/*.go ブラウザを確認すると、Spanなどが確認できるかと思います。 jaegerのUI めでたく、トレースできました。。。。。 しかしながら、 Span の情報が少なかったり、実装も少し冗長だったり、まだまだ実アプリケーションへの導入は難しそうです。 以降のセクションで少しずつ洗練していきましょう。 その他の実装リファレンス Tags/Logs/Baggage Span には Tag 、 Log 、 Baggage という付加情報をつけることができます。詳細は Tags, logs and baggage を参照してください。 このセクションではそれぞれの情報をどうやって付与するか、コード断片を紹介します。 SpanにTagをつける Span にはkey-value形式で任意の Tag をつけることが可能です。 tagValue := ... span.SetTag("tag.key", tagValue) また、 github.com/opentracing/opentracing-go/ext に Tag の変数があるので、適宜利用すると良いでしょう。 // Tagをつける例 ext.SpanKindRPCClient.Set(span) ext.SpanKindRPCServer.Set(span) ext.HTTPUrl.Set(span, "http://localhost:8080") ext.HTTPMethod.Set(span, http.MethodGet) ext.HTTPStatusCode.Set(span, http.StatusOK) SpanにLogを記録する github.com/opentracing/opentracing-go/log を使い、 Span 内で Log をつけます。 // Key-Value形式で簡易指定 span.LogKV( "hoge.log.key1", "hoge-log", "hoge.log.key2", "hoge-log2", ) // 型情報ありの指定 span.LogFields( tracelog.String("hoge.logfields.string", "hoge-log"), tracelog.Bool("hoge.logfields.bool", true), ) Log はこの関数を呼び出したタイミングで、 Span に印をつけるイメージです。 Baggageを使って情報を任意の情報を伝播する OpenTracingには(APIのI/Fを変更せずに)任意のオブジェクトを伝播する手段があります。 運ぶ情報を Baggage と呼びます。 強力な機能ですが、使い方には注意しましょう。 送信(client)側 baggage := ... span.SetBaggageItem("baggage", baggage) 受信(server)側 spanCtx, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header)) span := tracer.StartSpan("format", ext.RPCServerOption(spanCtx)) defer span.Finish() // Baggageの取得 baggage := span.BaggageItem("baggage") context.Contextを利用する アプリケーションをモジュール化して実装すると Span オブジェクトを引き回すのが辛いケースがあると思います。 そこで、OpenTracingのライブラリには context.Context 内に Span を含めるユーティリティが用意されています。 利用できるのであれば、コードがシンプルになるので context.Context 使う方が良いと思います。 Contextに Span をセット span := ... ctx := ... ctx = opentracing.ContextWithSpan(ctx, span) Contextから Span を取得 ctx := ... span := opentracing.SpanFromContext(ctx) Contextにある Span を親として、新しく Span を開始 ctx := ... // もし、ctxにSpanがない場合はGlobalTracerからSpanを開始する span := opentracing.StartSpanFromContext(ctx, "new-operation-name") サンプルアプリケーションを実装する これまでは、ミニマムな実装とコード断片で解説してきました。 前提となるコード知識はついてきたので、サービスの依存関係がすこしだけ複雑なアプリケーションのソースコードも見ていきます。 ソースコードを細かく解説すると長くなるので、解説はミニマムアプリケーションとの差分だけにします。 実装アプリケーションについて まずはアプリケーションの全体像です。 矢印の番号は呼び出し順序を表しています。 +--------+ | client | +---+----+ 1| ^ | | client | | side | +--------+------------------------------------------------- | server | | side | v | v +--------------+ 3 +--------------+ | hoge-service |---->| fuga-service | +---+----------+ +---+------+---+ 2| 4| 5| | | +---------+ | | | v v v +--------------+ +-------------+ +-------------+ | foo-service | | bar-service | | baz-service | +--------------+ +-------------+ +-------------+ この呼出関係をjaegerで見ることができればOKです。 サービスの実装 各サービスのHttpハンドラに Tag を付けていきます。 Tag については説明したとおりです。 http.HandlerFunc 実装のみの抜粋です。 // ./server/baz.go http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("baz") // SpanのExtract spanCtx, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header)) // リクエストからのSpanContextを引き継いで新しいSpanの開始 span := tracer.StartSpan("get_baz", ext.RPCServerOption(spanCtx)) defer span.Finish() // タグ付け ext.HTTPMethod.Set(span, r.Method) ext.HTTPUrl.Set(span, r.URL.String()) <-time.After(time.Second) w.WriteHeader(http.StatusOK) w.Write([]byte(`{ "name" : "baz!" }`)) // タグ付け ext.HTTPStatusCode.Set(span, http.StatusOK) }) ポイントは // タグ付け ext.HTTPMethod.Set(span, r.Method) ext.HTTPUrl.Set(span, r.URL.String()) ... omitted // タグ付け ext.HTTPStatusCode.Set(span, http.StatusOK) です。全ハンドラにこのコードを実装しておけば、 UIから見たときにフィルタしやすくなります。 サービス間のAPIコールの実装 クライアントの実装と同じことをすれば良いのですが、 今回の実装は Span を context.Context に入れて伝播します。 まずは、 context.Context にセットするコードです。 // ./server/hoge.go ... omitted // リクエストからのSpanContextを引き継いで新しいSpanの開始 span := tracer.StartSpan("get_hoge", ext.RPCServerOption(spanCtx)) defer span.Finish() ... omitted // ctxにセット ctx := r.Context() ctx = opentracing.ContextWithSpan(ctx, span) // APIコール _, _ = cli.Call(ctx, "foo") _, _ = cli.Call(ctx, "fuga") あまり難しくはないですね。 次はAPIコール部分の cli.Call の実装です。 context.Context から Span を開始する点がポイントです。 // ./server/client.go type ServiceClient struct { tracer opentracing.Tracer } func (s *ServiceClient) Call(ctx context.Context, serviceName string) (int, map[string]interface{}) { // ctxにセットされているSpanから新しく子のSpanを開始する span, _ := opentracing.StartSpanFromContextWithTracer(ctx, s.tracer, fmt.Sprintf("call_%s_%s", http.MethodGet, serviceName)) defer span.Finish() serviceURL, _ := url.Parse(fmt.Sprintf("http://localhost:%d", portMapping[serviceName])) req, _ := http.NewRequest(http.MethodGet, serviceURL.String(), nil) // Tag付け ext.SpanKindRPCClient.Set(span) ext.HTTPUrl.Set(span, serviceURL.String()) ext.HTTPMethod.Set(span, "GET") // Tracerを使ってSpanの情報をInject _ = span.Tracer().Inject( span.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header), ) resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() var responseJson map[string]interface{} json.NewDecoder(resp.Body).Decode(&responseJson) return resp.StatusCode, responseJson } context.Context から Span を開始するのはこの部分です。 // ctxにセットされているSpanから新しく子のSpanを開始する span, _ := opentracing.StartSpanFromContextWithTracer(ctx, s.tracer, fmt.Sprintf("call_%s_%s", http.MethodGet, serviceName)) defer span.Finish() 簡単ですね。 さて、これで準備はできました。 動作確認 Jaegerを起動した状態で サービス群を起動します。 go run ./server/*.go クライアントを実行します。 go run ./client/*.go そして、 localhost:16686 にアクセスしてみましょう。あえて、スクショははりません。ぜひ動かしてみてください! サービス間の呼び出しがこんな感じで見ることができたらシステムの運用ちょっと楽になりそうですよね。 実装をもっと楽したい サーバー側、クライアント側のコードを見ていただくと分かると思いますが、 まだ少し冗長です。 Tag をつける部分とか。。。 go-stdlib のライブラリを使うと、 もう少し処理を共通化することができます。 リポジトリを覗いてみると、サーバー側はミドルウェアの実装、クライアント側はトランスポートの実装があるので、 適宜利用すると良いでしょう。 但し、まだ実験段階のようなので、使うときには注意しましょう。 最後に 少し長い記事となってしまいました。 OpenTracing x Go の理解の一助となれば幸いです。 ここまでくれば、もっと実践的なアプリケーションサンプル Hot R.O.D. - Rides on Demand も読み解きやすいと思います。 また、導入する際は Best Practices もあるので、 このページも必読です。 話は変わりますが、弊社では サーバーサイドエンジニアを随時募集中です! 私達の取り組みに興味があればぜひ気軽に遊びに来てください。 以上、アドベントカレンダー8日目担当の miyauchi でした! 明日は弊社のエンベデッドチームの要、 ochiai さんです! 参考リンク集 記事中のコード -> https://github.com/aptpod/opentracing-go-sandbox OpenTracing オフィシャルサイト -> https://opentracing.io/ OpenTracing ユーティリティライブラリ(実験段階) -> https://github.com/opentracing-contrib/go-stdlib jaeger オフィシャルサイト -> https://www.jaegertracing.io/ jaeger Github -> https://github.com/jaegertracing/jaeger jaeger client Github -> https://github.com/jaegertracing/jaeger-client-go
答え:トラベルルーターを使うと、手持ちのPC・スマホのWebブラウザからポチポチするだけで設定を変えられる!! 先端技術調査グループの南波です。 aptpod Advent Calendar 2019 の7日目はちょっとした便利ガジェットのお話です。 背景 1日目 、 2日目 でも紹介あったように、先端技術調査グループでは「自社技術・製品と世の中の技術・製品(機械学習、AWS Robomakerなど)を組み合わせ、新たな気付きをもたらすようなデモを作る」こともミッションの1つとして取り組んでいます。 デモの多くは展示会駆動開発(TDD 😎)で作られるため、 1日目 のTurtlebot3内蔵のもの、 2日目 のWebカメラのUSB接続先など、ラズパイを使う場合は展示会会場に引かれたLANに有線で接続するだけ、という前提で社内での動作検証等も行うことができます。 課題 まずは展示会をターゲットに作るのですが、bizdevチームから「この前のデモ、次の訪問打ち合わせに持ってきたいんだけどできるかな?ネットワークはモバイルルーターあればいい?」と2次利用依頼されることもあり、この場合は接続先の情報に合わせてラズパイを設定することになります。 ラズパイの無線ネットワーク接続の設定を変更するとき、基本的には ディスプレイ/キーボードを直接刺して編集する 手元のPCと同じネットワークに繋ぎ、sshで入って編集する のどちらかを行うことになるかと思います。 環境の整った社内で実施する場合はこれで問題ないのですが、「持って行ったモバイルルーターが調子が悪いので、お客様がお持ちのゲスト用無線LANなどの別のネットワークに変更したい」といった事情が発生した場合に苦しくなります。 解決策 トラベルルーターを使います。トラベルルーターは名前の通り、 小型軽量 ホテルなどに敷設された有線LANを、イーサネットの口のない手持ちのスマホ・ラップトップから利用できる といった特徴を備えたいわゆる無線ルーターです。さらに(今回検証に利用したBuffaloの機種では「ワイヤレスワンモード」という名前の) ホテルなどに敷設された無線LANを、イーサネットの口のある手持ちのデバイスから利用できる という機能もあり、「小型軽量」という特徴と合わせて今回の課題の解決にぴったりハマりました。 動作検証 ① Buffaloの WMR-433W2 を購入 ② 電源を入れ、手持ちのPCから筐体裏の情報(SSID / Password)に従い接続 ③ ウェブブラウザから 192.168.13.1 にアクセスし、動作モードおよびWAN側の設定を実施 トラベルルータ設定画面-1 トラベルルータ設定画面-2 トラベルルータ設定画面-3 ④ 手持ちのPCからインターネットに接続できていることを確認 🎉 ⑤ ラズパイとトラベルルーターを接続 イーサネットで有線接続、または $sudo raspi-config 等から無線の設定を入力して接続 ⑥ ラズパイからインターネットに接続できていることを確認 🎉 上記で一度動作確認できれば、以降は ② ~ ④ の手順だけを行うだけで、ラズパイから任意のネットワークを利用することができるようになります。 まとめ aptpod Advent Calendar 2019 7日目はラズパイの出先利用をちょっと便利にするデバイスのご紹介でした。 1つ懸念としては、「あくまで1,2個のデバイスを短時間接続する」目的での利用を想定した選択のため、「より多くのデバイスを長時間連続で」といったケースではまた別の選択が必要になるかもしれません。今後その課題に直面した際にはまたなにか記事にできればと思います💪
aptpod Advent Calendar 2019 6日目担当のエンベデッドチーム 久保田です。 仕事で関わることの多い自動車関連の技術について、少しお話させていただこうと思います。 intdash Automotive Pro は、自動車産業における車両CAN(Controller Area Network)データのデータロギング、データ管理、可視化・解析などのワークフローをクラウドシステムをベースにワンストップで実現するSaaSソリューションです。 このソリューションで車両CANを扱っていますが、CANの拡張型プロトコルであるCAN FD (CAN with Flexible Data rate)についてはご存じない方もいらっしゃるのではないでしょうか。 CAN FDについて調べると、ハードウェア仕様の話から始まる難しい記事ばかりです。 そこで、ソフトウェアエンジニア目線でCAN FDについてまとめてみました。 CAN FDについて、ざっくり知りたい方向けです。 CAN (Controller Area Network) CAN FDの話の前に、CANをおさらいしておきます。 CANとは、ドイツのBosch社が開発したシリアル通信プロトコルです。 OSI参照モデルの物理層・データリンク層・トランスポート層にあたります。 ライン型バス 複数のノード接続によるネットワークを構成するため、CANはライン型バスを採用しています。 転送速度 転送速度の最大は 1 Mbpsです。 フレーム バスに流れる信号は5つのFieldで構成されています。 フレーム機能 フィールド名 説明 Arbitration field 通信調停 Control field 通信制御 Data field 転送データ CRC field 誤り検出 Acknowledge field 受信完了通知 フレーム構成 ID (標準ID:11 bits, 拡張ID:29 bits) データ長(DLC) データ (最大 8 bytes) マルチマスター方式 バスに空きがある場合、ライン型バスに接続された全てのノードはフレームを送信できます。 ノードはライン型バスに接続された全てのノードにフレームを送信できます。 ( ブロードキャスト ) フレームを受信した場合は全てのノードが受信完了を通知できます。( ACK ) CSMA/CA方式バスアクセス バスに早くアクセスしたノードがフレームを送信できます。 また、同時に複数のノードがフレームを送信開始した場合、優先順位の高いID (ID番号の小さい方) が送信できます。 CAN FD(CAN with Flexible Data-Rate) CAN FDとは、CANプロトコル仕様を拡張し、従来のCANよりも通信速度の高速化と送受信データの大容量化に対応可能な通信プロトコルです。 従来のCANと物理層、システム構成がほぼ同等 物理層のコントローラ、トランシーバはCAN FD対応必要 IDは「標準フォーマット(11ビットID)」と「拡張フォーマット(29ビットID)」の2種類 (従来と同等) データ長は 最大 64 bytes (従来は最大 8 bytes) 通信ボーレートは最大 1 Mbps (従来と同等) データ部分の転送速度が可変で 通信ボーレートの1 Mpbs以上 が可能 (従来は通信ボーレートと同等) CAN FD種類 non-ISO CAN FD (The original Bosch CAN FD) ISO CAN FD (CRC強化による堅牢性改善) CAN FDフレーム構成 ID (標準ID:11 bits, 拡張ID:29 bits) データ長(DLC) データ ( 最大 64 bytes ) CAN FDデータ長 (bytes) フレームに指定されたDLCによりデータ長が指定できます。 CANが最大 8 bytesに対して、CAN FDは最大64 bytesです。 DLC 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 CAN 0 1 2 3 4 5 6 7 8 8 8 8 8 8 8 8 CAN FD 0 1 2 3 4 5 6 7 8 12 16 20 24 32 48 64 通信速度 通信調停を行うnominal(arbitration)ビットレートとデータを転送するdataビットレートとを異なるビットレートに設定が可能です。 nominalビットレートは最大 1 Mbpsですが、 例えば、dataビットレートは制御用途では 2 Mbps、診断・リプログラミング用途では 5 Mbpsで使用されます。 通信パフォーマンス 種類 CAN転送速度(kbps) Data転送速度(Mbps) 送信フレーム数(/sec) CANデータ長(bytes) busload(%) CAN 500 - 4000 8 99 CAN FD 500 2 4000 8 46 CAN FD 500 2 4000 32 99 通信速度の高速化と送受信データの大容量化により、データ転送効率が改善されています CANノードとCAN FDノードの混在 CANノードとCAN FDノードが同一バス上に混在可能です。 ただし、CANノードはCAN FDフレームを受信できません。 送信フレーム CANノード受信 CAN FDノード受信 CAN フレーム OK OK CAN FD フレーム Error OK まとめ CAN FDについて、おわかりいただけましたでしょうか。 Linuxでは、SocketCanと呼ばれるsubsystemにてCAN FDがサポートされていますので CAN FD対応トランシーバを簡単に接続することができます。 実際に動作させて、CAN FDの優位性を確認してみてください。 参考 CAN in Automation (CiA): CAN FD - The basic idea SocketCAN - Controller Area Network — The Linux Kernel documentation
本ブログへいらっしゃったみなさま、初めまして。 aptpod Advent Calendar 2019 5日目担当のsetoです。入社4ヶ月(2019/12/05現在)で、機械学習系の案件や自社プロダクト付加価値向上のための技術調査に従事してます。社内メンバーの技術領域が多彩で刺激を受けながらお仕事しています。 本記事では、エッジデバイスに採用したJetson Nanoの出力結果とクラウド推論に採用したAmazon SageMakerのエンドポイントの出力結果が同じにならない問題に遭遇し、解決するのに苦労した話です。 この記事を要約しますと、"読み込んだ画像のピクセル値が環境が違うと同じにならない"という検証すればすぐわかる問題に自分の思い込みのせいでなかなかたどり着けなかったという経験談を書いております。 取り組みの背景 弊社が開発を進めているシステムは、ディープラーニングの推論をクラウドとエッジデバイスの別々の環境でおこなっています。推論結果の一貫性を保つため同じ入力で同じ結果が出力できるように調査と対応を行うのが取り組みの背景です。 具体的には、クラウドの推論にAmazon SageMakerでApache MXNetを使用した機能を利用し、エッジ側はAmazon SageMaker NeoとNeo-AI-DLRを利用しました。この取り組みのゴールはこれらの異なるフレームワークから出力される推論結果を一致させることです。 使用しているフレームワーク Amazon SageMaker クラウド側の推論は Amazon SageMaker (以下、SageMaker)を活用しています。本テックブログの 2日目のキシダさん も同じフレームワークを活用しており、こちらの記事もご覧いただけると理解が深まると思います。本ブログでは、 SageMaker上でApache MXNetを動作させる枠組み を用いたものを利用しています。 Apache MXNet (以下、mxnet)は,ディープラーニングのフレームワークの一つです。 Amazon SageMaker Neo エッジ側の推論は Amazon SageMaker Neo (以下、SageMaker Neo)とSagMaker NeoでコンパイルしたモデルをJetson Nanoで動作させるランタイム Neo-AI-DLR (以下、dlr)を活用しています。本ブログでエッジ側で動作させる場合は、これらのフレームワークを活用して動作させていることになります。 調査の詳細 今回、調査した推論機能のパイプラインは下図のとおりです、特殊なことはなにもしていません。 図1. 推論処理パイプライン 調査をおこなう際にSageMakerのエンドポイントを立て続けて検証することはコストが高かったため、同一の結果がでることを確認したGPU付きのローカルマシンを代わりに使用しました。 調査の流れは、 推論処理の一致確認(計算が複雑で一致が難しいと判断したためはじめに対応) 前処理の結果の一致確認(1.よりは簡易であると考え2番目に対応) 画像の読み込みの一致確認(予想外の対応) という項目を行いました. 3. は予想しておらず、 2. が問題だという思い込みが苦労した大きい要因でもあります。調査に思い込みは厳禁であることを溶かした時間で学びました😇 推論処理の一致確認の詳細 この調査のゴールは、入力データを同じにすればエンドポイントで利用しているmxnetとエッジ側のdlrの推論結果の一致を確認することです。手続きとして以下の2段階の分けて確認を行いました。 全て1にした配列を入力して出力結果が一致するかを確認(一様な入力で一致するのか) 0.0~1.0までのランダムな数字で出力結果が一致するかを確認(多様な入力で一致するのか) この検証の際は、並列計算の加算が多く行われているため全ての数字の一致は望めないため、 1e-5 の桁まで数値が同じであれば一致しているとしました。これらの調査の結果、クラウド側のSageMakerの推論結果とエッジ側のdlrを用いた推論結果は一致することがわかりました。つまり、 推論に入力する前処理済みのデータが一致していない ことがわかりました。 前処理の結果の一致確認の詳細 この調査のゴールは、同じ実装を使った前処理の出力結果の一致を確認することです。前処理は、どのデバイスでもだいたいpipで入れることができる、pillow(6.1.0)ライブラリを使って実装しました。 図1. から前処理はリサイズとクロップをおこなう処理となります。前処理は、画像でみると 図2. のように変形処理を施していきます。 図2.前処理のイメージ図 確認方法は、サンプル画像を入力し結果が一致するかを調査しました。結果は出力の配列が不一致となりました。このため、個々の処理を調べる必要があると判断し、はじめに出力に近いリサイズの調査を行いました。この結果、同じバージョン、同じ補完方法でも結果が一致しませんでした。このため、他のライブラリを使ってみたり、githubのソースコード見にいったり色々調査をしましたが、これだと強く主張できる原因を見いだせていませんでした。ここで大きく時間をかけてしまいました。このときに自分の考えにバイアスがあり入力に着目するまで考えが及ばなかったことが本件の時間浪費の原因でした。バイアスよくない😰 画像読み込みの一致確認の詳細 何がきっかけか覚えていませんが、そもそも読み込んだ画像データに違いがあれば、 前処理の結果が一致するはずがない と思い至り、入力画像を読み込んだデータで比較しました。その結果、読み込んだ画像のピクセル値が一部一致しないことがわかりました。 ついに原因を特定することができました! 以下、公開して良いデータを使って一致しない結果を再現しました。 ※調査当時はGPUサーバーを使いましたが、MacBook Proも画像読み込みの結果がGPUサーバーと一致したので、再現はMacBook Proを使用しています。 以下、エッジ側で画像を保存するためのスクリプトです。非常に簡易なスクリプトでpillowで画像を読み込んだ後にndarrayに変換しnpyで保存しています。このnpyファイルをMacBook Proに持ってきて比較を行いました。 #!/usr/bin/env python # coding: utf-8 import argparse import numpy as np from PIL import Image if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('-f', '--file-name', required=True, type=str) args = parser.parse_args() img_dir = './sample-images/IMG-0269.JPG' img = Image.open(img_dir) img_arr = np.array(img) store_name = args.file_name np.save(store_name, img_arr) 以下、比較したnotebookです。7セル目のFalseが比較結果になります。 このの経験からpython上で扱うライブラリのバージョンが同じでも、結果が一致する保証はないという教訓を得ることができました。 notebook 問題はどのように対処したのか エッジ側はライブラリを色々インストール、アンインストールしても変えることができなかったので、クラウド側の画像読み取りを変えました。pillowではなくmxnetでの読み込みに置き換えたら画像のデータが一致したので、mxnetでの読み込みを採用しました。notebookの8セル目と9セル目の結果が再現となっています。 調査しきれなかったこと,わからなかったこと おそらくpillowとmxnetが画像読み込み時に利用しているlibjpegのバージョンなどが異なっていることが原因であると判断しました。しかし、エッジ側のlibjpegのバージョンを変更しても解決できませんでした。このため、きちんと原因を追い切れているわけではなく、再現しない可能性もあります。 まとめ 異なるフレームワーク、ライブラリを使って推論結果をあわせるのは予想以上に苦労しました。ディープラーニングのフレームワークをまたいで処理結果を近似することは経験していてそこが一番苦労すると思っていたことが落とし穴でした。また、データ読み込みに一貫性がないわけがないとう確信がより問題の特定に時間をかけてしました。これを教訓に調査する、比較するといったときはこうあるだろうなどのバイアスをなるべく持たないようにしたいと思います。 最後に,本ブログをここまで読んでいただきありがとうございます。これからも苦労したことや検証してみた内容などを引き続きブログにアップロードしていきたいと思います!
はじめに aptpod という会社のQAチーム横田です。 Aptpod Advent Calendar 2019  4日目の記事になります。 去年に引き続き、アドベントカレンダーを書いています。 去年は、テスト自動化について書きました。 qiita.com 今回は、QA(品質保証)とは何かを考えた話を書いていきたいと思います。 * QA = Quality Assurance(以下、QAと省略する) QAって何をするの はじめに、QAの一般的な定義を明記しておきます。 品質保証(ひんしつほしょう、英: quality assurance、QA)は、効率と品質が求められるあらゆる活動において、それらに保証を与えるのに必要な証拠を提供する活動一般を指す。計画され体系化された活動は一般に、その製品やサービスが要求された品質を満足していることを保証する必要がある。品質保証は品質管理と密接に関連しており、これらによって顧客や権利保有者のニーズ・期待・要求に製品が適合していることを保証する。QA は、品質が所定のレベルに到達していることを事前に確認する手続きを効率的に構築するものである。 QA は設計・開発・製造・実装・サービス・文書といったあらゆる活動をカバーする。また、QA には材料や部品、製造工程や検査工程などの品質の規定も含まれる。 Wikipediaより引用 品質保証 - Wikipedia 品質とは 本題のQAとはの前に、そもそも「品質」とは何を指すのかを自分なりに考えました。 千差万別の考え方があり、これが答えだとは思っていません。 あくまで、今回考えた際に、私なりにしっくりきた内容としています。 改めて考える前までも、「品質」という言葉は当たり前の様に使っていました。 その時は、 良い品質 = ユーザが心地よく使えること ぐらいの感覚でいました。 そこに、開発者含む提供する側の事は一切考えていません。 「開発者が疲弊した結果ユーザが心地よくなったら、それは良い品質なのだろうか?」(哲学的・・・) 自問自答した結果、答えは 『NO』 でした。 結論 「ユーザーも企業も安心していられる状態」が今の所一番しっくりくる答えになりました。 どんな状態か ユーザー 何の不便も不安も無く使用できている →言わずもがなですが、何の障害も無くユーザビリティ的にも不自由無く使用できている状態です。 企業 次に向かって進んでいる →例えば、リリースしたモノに対して、何も心配しなくて良い状態です。 じゃ、QAは何をするの? 「品質」の定義ができたので、本題のQAは何をするのかに話を戻します。 QA=品質保証=「ユーザーも企業も安心していられる状態」を整える活動 を指す。 これが現時点での私なりの答えになります。 リリースまでは、ユーザーが安心できる状態を「テスト」という手段で作っていきます。 リリース後は、PMや開発メンバーが何もせずに次のプロダクト開発ができる様な環境を作っていきます。 例えば、問い合わせが発生したバグの一次請けだったり、リグレッションテストを定期的に行うなどして 心配事を少しでも減らしていく活動を進めてい行くことかなと思います。 よく、QAやテストエンジニアは「最後の砦」と言われがちですがユーザーと企業と並走して進んでいくことが理想と考えています。 そんな活動を少しでもしていきたいなと思います。 まとめ こういった活動していくには、多彩な知識が必要になってきます。 テストの事だけでなく、開発手法や工程についての知識も。 また、限られたリソース(人員/時間など)内で行うには、自動化スキルだったりも必要になってきます。 もちろん、様々な人達と会話することが必然的に求められていくので、コミュニケーション能力も大事。 そういった成長ができる職種がQAとしての醍醐味かなと思っています。
この記事は Aptpod Advent Calendar 2019 の3日目の記事です。 先端技術調査グループの大久保です。 最近はWebAssemblyが注目されるようになり、弊社でもWebフロントエンド側での軽量化・高速化に応用できないか検討をしています。 そこで、今回はWebSocketのechoクライアントをRustとGoで作成し、wasmへコンパイルした時のファイルサイズを比較してみます。現状では、wasm内から直接WebSocketを取り扱う方法は無いらしいので、JavaScriptのAPIを呼び出してWebSocket通信を実現します。 Goでの実装 私はGoが書けないため、先輩からもらった以下のコードを使わせてもらいました。適当にwstest.goと名前を付けて保存します。 package main import ( "fmt" "syscall/js" ) func print (i []js.Value) { fmt.Println(i) } func websocket_test(this js.Value, args []js.Value) interface {} { fmt.Println( "start websocket_test" ) ws := js.Global().Get( "WebSocket" ).New( "wss://echo.websocket.org/" ) ws.Set( "onopen" , js.FuncOf( func (this js.Value, args []js.Value) interface {} { fmt.Println( "open" ) ws.Call( "send" , "hello world" ) return nil })) ws.Set( "onmessage" , js.FuncOf( func (this js.Value, args []js.Value) interface {} { data := args[ 0 ].Get( "data" ) fmt.Println( "Received =" , data) fmt.Println(args, args[ 0 ], data.Type(), data.String()) ws.Call( "close" ) return nil })) ws.Set( "onerror" , js.FuncOf( func (this js.Value, args []js.Value) interface {} { fmt.Println( "Error" ) return nil })) ws.Set( "onclose" , js.FuncOf( func (this js.Value, args []js.Value) interface {} { fmt.Println( "WebSocket connection closed." ) return nil })) return nil } func registerCallbacks() { js.Global().Set( "websocket_test" , js.FuncOf(websocket_test)) fmt.Println( "After callback register" ) } func main() { c := make ( chan struct {}, 0 ) registerCallbacks() <-c } index.htmlを次の内容で用意します。 <!doctype html> < html > < head > < meta charset = "utf-8" > < title > Go wasm </ title > </ head > < body > < script src = "wasm_exec.js" ></ script > < script > const go = new Go () ; WebAssembly.instantiateStreaming ( fetch ( "test.wasm" ) , go.importObject ) .then (( result ) => { go.run ( result.instance ) ; } ) ; </ script > < button onClick="websocket_test () ;" id = "runButton" > Run </ button > </ body > </ html > 次のコマンドでコンパイルします。 GOOS=js GOARCH=wasm go build -o test.wasm あと、wasm_exec.jsを用意します。 curl -sO https://raw.githubusercontent.com/golang/go/master/misc/wasm/wasm_exec.js これらの用意したファイルがあるディレクトリでサーバを立ち上げます。 python3 -m http.server 8080 Webブラウザで http://localhost:8080/ にアクセスすると、ボタンを押すたびに次のようにコンソールへ出力されます。 open Received = hello world [<object>] <object> string hello world WebSocket connection closed. エコーサーバにアクセスし、メッセージが返ってきていることが確認できます。 Rustでの実装 RustからWasmのHelloWorldは以下のドキュメントの通り行えば問題ありません。もちろんRustの処理系はあらかじめインストールしておく必要があります。 https://rustwasm.github.io/docs/book/game-of-life/hello-world.html WasmからWebSocketを利用する方法は、以下参考。 https://rustwasm.github.io/docs/wasm-bindgen/examples/websockets.html Goの方の実装に合わせて少々変えたので、以下ソースコードを記載します。まずはlib.rsから。 #![no_std] extern crate alloc ; use wasm_bindgen :: prelude :: * ; use wasm_bindgen :: JsCast; use web_sys :: {ErrorEvent, MessageEvent, WebSocket}; use alloc :: boxed :: Box ; use alloc :: string :: ToString ; #[global_allocator] static ALLOC: wee_alloc :: WeeAlloc = wee_alloc :: WeeAlloc :: INIT; #[wasm_bindgen] extern "C" { // console.log()をインポートする #[wasm_bindgen(js_namespace = console)] pub fn log (s: &str ); } macro_rules! console_log { // println!()風にconsoleへ出力するためのマクロ ( $($t :tt )* ) => ( log ( & format_args! ( $($t)* ). to_string ())) } #[wasm_bindgen] pub fn websocket_test () -> Result < (), JsValue > { // echoサーバにつなげる let ws = WebSocket :: new ( "wss://echo.websocket.org" )?; // コールバックを登録していく let onmessage_callback = Closure :: wrap ( Box :: new ( move | e: MessageEvent | { let response = e . data () . as_string () . expect ( "Can't convert received data to a string" ); console_log! ( "message event, received data: {:?}" , response); }) as Box < dyn FnMut (MessageEvent) > ); ws. set_onmessage ( Some (onmessage_callback. as_ref (). unchecked_ref ())); onmessage_callback. forget (); let onerror_callback = Closure :: wrap ( Box :: new ( move | e: ErrorEvent | { console_log! ( "error event: {:?}" , e); }) as Box < dyn FnMut (ErrorEvent) > ); ws. set_onerror ( Some (onerror_callback. as_ref (). unchecked_ref ())); onerror_callback. forget (); let cloned_ws = ws. clone (); let onopen_callback = Closure :: wrap ( Box :: new ( move | _ | { console_log! ( "socket opened" ); match cloned_ws. send_with_str ( "ping" ) { Ok (_) => console_log! ( "message successfully sent" ), Err (err) => console_log! ( "error sending message: {:?}" , err), } }) as Box < dyn FnMut (JsValue) > ); ws. set_onopen ( Some (onopen_callback. as_ref (). unchecked_ref ())); onopen_callback. forget (); Ok (()) } web_sysというクレート以下にJavaScriptのオブジェクトにアクセスするための各種APIが用意されているので、それを利用します。また、 #[wasm_bindgen] という属性をつけるだけで、RustとJavaScriptの間で関数やオブジェクトを相互にやりとりできるようにしてくれます。ここでは、 console.log() をRustから利用できるようにしたり、 websocket_test() という関数をJavaScript側から利用できるよう設定します。この websocket_test() が呼び出されると、WebSocketのインスタンスを作成し、on_openなどの各種コールバックを設定していきます。JavaScriptの関数やオブジェクトをシームレスに扱えるので、Goよりも洗練されている印象です。ただしクロージャまわりは若干トリッキーに書く必要があるようです。Rustのライフタイムを理解していないと意味不明かもしれません。 また、よりサイズを小さくするため #![no_std] を指定し、標準ライブラリが含まれないようにします。また、メモリアロケータに WeeAlloc を指定します。これはwasm向けのよりサイズの小さいメモリアロケータです。 index.htmlではボタンを配置します。 <!DOCTYPE html> < html > < head > < meta charset = "utf-8" > < title > Hello wasm-pack! </ title > </ head > < body > < noscript > This page contains webassembly and javascript content, please enable javascript in your browser. </ noscript > < script src = "./bootstrap.js" ></ script > < button id = "runButton" > Run </ button > </ body > </ html > index.jsにJavaScriptを書いていきます。ここでは、ボタンがクリックされた場合に、lib.rsで定義した websocket_test() を呼び出すようにします。 import * as wasm from "wsecho-client" ; document .getElementById( "runButton" ).onclick = function () { wasm.websocket_test(); } ; npm start でサーバを立ててアクセスすると、ボタンを押すたびにコンソールに次のような表示が出ます。 socket opened message successfully sent message event, received data: "ping" エコーサーバにメッセージを投げて、返ってくることを確認できました。 ファイルサイズの比較 RustとGoそれぞれで生成されたwasmファイルを比較してみます。どちらも生成されるwasmファイルは1つのみですが、wasmをロードしたりするためのJavaScriptファイルがいくつかついてきます。JavaScriptファイルのサイズはどちらも10KB程度ですが、Rustの方はJavaScript向けにバインディングを生成する関係上、サイズが変化する可能性があります。ちなみにRustからはTypeScript用のバインディングも勝手に生成してくれるというオマケつき。 wasmファイルのサイズは以下の通りです。 Rust Go wasmファイルのサイズ(バイト) 42875 2276691 Goはランタイムが重いせいか、Rustのほうが圧倒的にサイズが小さいです。 サイズを最適化 UNIXには実行ファイルを小さくする(シンボル情報を削除する)ための strip コマンドがありますが、似たようなものがwasmにも用意されています。以下の2つのツールを使ってサイズを最適化してみます。 wasm-strip ( https://github.com/WebAssembly/wabt ) カスタムセクション(シンボル情報みたいなもの)を削除してくれるツール。 wasm-opt ( https://github.com/WebAssembly/binaryen ) 用途に応じてwasmを最適化してくれるツール。サイズ最適化には -Os オプションを使う。 これらを適用した結果が以下の表になります。 (単位はバイト) Rust Go オリジナル 42875 2276691 wasm-strip適用 30649 2226331 wasm-opt適用 26663 2161744 両方適用 26538 2161607 どちらもサイズの削減ができましたが、Goの方は2MBを割ることはできませんでした。GC等のランタイムが入っているとしたらこんなものなのでしょうか。tinygoというものを使えばずっと小さくできるらしく、試したところwasmファイルの生成まで行きましたが、Webブラウザでアクセスした時にエラーが出たので今回は割愛します。 ちなみに、wasm-optはデフォルトでカスタムセクションを削除するのでwasm-stripを重ねて適用する必要は無いはずですが、wasm-strip -> wasm-opt の順で適用すると、wasm-optだけ適用した時と少しサイズが異なってました。 まとめ wasmのファイルサイズを見ると、RustはGoよりずっと軽量 RustとJavaScriptのやりとりはかなりシームレスに書けるようになっている。 wasmのバイナリをいじるツールもリリースされており、サイズ圧縮もできる。 WebAssemblyについては引き続き調査を行って参ります!
aptpod Advent Calendar 2019 2日目担当のキシダです。もともとただのエンジニアだったのですが、半年前ほどこちらに入社して、今では機械学習系の案件やデータ解析向け製品の開発に従事してます。周りの人も心優しい人ばかりで、楽しくお仕事しています。 (職種はデータサイエンティストですが、他にもいろいろやらせていただいてます) よろしくお願いいたします。 さて本記事では、私が入社したての頃に弊社の機械学習系案件の技術紹介の一貫で、 お菓子のリアルタイム検出システムデモ をAmazon SageMakerと自社製品を使って構築することになり、その際に奮闘した記録をこの場をかりてご紹介したいと思います。 このデモは、今開催されている 『AWS re:Invent 2019』 にも展示されているので、もしラスベガスにいらっしゃる方、ぜひ弊社 aptpod のブースまで! ※Amazon Sagemakerについてはこちらへ Amazon SageMaker(機械学習モデルを大規模に構築、トレーニング、デプロイ)| AWS どんなシステム? 当初の願望としてはこんな様子でした。 アプトポッドのロゴ入りお菓子(かわいい)をカメラにうつすと、お菓子の種類をシステム側が検出 検出結果はリアルタイムで弊社製品の可視化ツール Visual M2M (後述に簡単な説明記載)上で常に表示されている お客さんがお菓子をとったりおいたりすると、それに伴って Visual M2M 上でシステムが反応するので楽しい かわいいお菓子ももらえてさらにハッピー お客さんがよろこんで僕もハッピー みんなハッピー ・・・ という感じで作りたいね、となりました。 その結果・・・ 一ヶ月足らずで以下のようなデモができました。 Visual M2Mで写すお菓子たち お菓子の撮影現場 弊社デザインチームがデザインした主役のおかしたち。うん。かわいい。 意外に好評なデモの展示会場 と、イメージとしてはこのような感じです。 このデモで使用する自社製品/サービス 僭越ながら、今回のデモで使用している製品をざっくり紹介させていただきます。 intdash aptpodの主力製品である『intdash』 はご存知ですか?? 弊社HP( https://www.aptpod.co.jp/products/ )ではこう記載されています。 intdashとは、世界高速レベルの100ミリ秒∼1ミリ秒間隔程度の高頻度で、 発生する時系列データを品質保証のないネットワークを経由して、 高速・大容量かつ安定的にストリーミングするための双方向データ伝送プラットフォームです ちょっとあまりピンとこない方が多いかもしれませんが 「ものすごい速く、しかもデータが欠けることなく双方向に通信できるプラットフォーム」 とイメージいただければよいかと。。。   このブログでもintdashについて触れているのですが、より詳細を知りたいという方がいらっしゃいましたら こちら をご参照ください! Visual M2M 既に上記で紹介しましたが、今回の検出結果の可視化には『intdash Visual M2M(通称VM2M)』( https://www.aptpod.co.jp/products/vm2m/ ) と呼ばれるイケてる可視化ツールを使います。 intdash Visual M2M intdash Analytics Services intdashのみだとデータを双方向にストリーミングするため、通常はデータ通信途中のデータをストリーミングで解析・加工することができません。そこで、『intdash Analytics Services』 と呼ばれる「データの解析用実行基盤」も提供しています。この製品を使うことで、以下のユースケースに対応できます。今回は、お菓子を撮影した動画データを加工する基盤として使っています。 時系列データの計算処理・分析処理を実装したコードをintdashに反映し、処理することができる 機械学習環境の構築にも適用でき、高速な推論パイプラインを回す などなど システムのアピールポイント 前述も触れてますが、このシステムのポイントは リアルタイム性 です。 ローカルデータを分析して可視化するというパターンとは違い、動画の撮影中と同時にシステムはお菓子を検出し、UIに表示してくれるというわりと難しめなシステムを短期間で開発できることがこのデモのアピールポイントです。 intdashべんり。 展示会のコンテンツとしての旨味 お菓子が動画に映ると、システムがリアルタイムで反応して、「動いてる動いてる!!!」となって楽しい アプトポッドとしてのアピールポイント お客様向けには、欠損なくリアルタイムで機械学習の推論が回せることと、その開発が容易であることをアピールしました。 intdash Analytics Serviceによって、外部サービスと連携してストリーミングデータの加工を容易にする 加工を行っても、データ通信のリアルタイム性を担保する 開発者側は加工処理をスクリプトを書くだけで、あとはAPIを叩くだけでデータのストリーミングシステムの簡単に組み込むことができる 実現方法 いよいよ本題です。ここでは本システムを構築した手順をざっと記し、次項でより詳細な奮闘記を記します。 今回のシステム構築に伴い使用したサービスとその関係は以下の図の通りです。 使用サービスとその流れ 実現までのおおまかな手順 手順イメージ 通常の動画データを画像で切り出し、Amazon SageMaker Ground Truth にてアノテーションしトレーニング用データを作る Amazon SageMakerを使用してモデルをトレーニング モデルを通して推論できるようなエンドポイントを構築する(このエンドポイントに画像を渡すと、検出結果が返ってくるようになります) intdashから取得した動画データをSageMakerのエンドポイントに投げて、検出結果を可視化するスクリプトを、intdash Analytics Serviceにデプロイする これができればあとはWebカメラから実際に繋げてVM2Mで見ればOKです。 手順2,3箇所はこちらを参考にしました:  SageMakerで「うまい棒検出モデル」を作ってみた   https://dev.classmethod.jp/cloud/aws/sagemaker-umaibo-object-detection/ トレーニング用データの作成 お菓子の撮影 まずはお菓子の画像を撮影します。方向を45度ずつかたむけたり、複数お菓子を映したり、関係ないお菓子をいれたり、試行錯誤を続けました。 撮影したお菓子たち アノテーションの実施 実際に教師データ(正解データ)をつくるため、撮影したデータに対して、「どこに何がうつっているか」をツールを使って定義していきます。いわゆる アノテーション というものです。 今回は、AWSで展開されている、Amazon SageMaker Ground Truthを使用します。 実際には以下のように、対象のお菓子に対して四角(Bounding Box)で囲っていきます。 アノテーション実施中のUI ※Amazon SageMaker Ground Truthの詳細はこちらから   https://aws.amazon.com/jp/sagemaker/groundtruth/ 細かな手順を記載すると膨大な文章量となるので、こちらは割愛し個人的なGround Truthのお得ポイントをこちらにまとめておきました Amazon SageMaker Ground Truth お得ポイント せっかくなのでいくつか補足します。 1. ラベリングの種類が豊富 Ground Truthは作りたいモデルによって様々なアノテーションの種類が用意されています。また、自分用にアノテーションツールのカスタマイズもでき、自由度がだいぶ高いです。 2. ラベリングの作業をGround Truthがよしなにメンバーに割り振ってくれる ここでのGoodポイントは、 管理者はアドレスを追加するだけで一発で作業環境を提供できる ことです。IAMとかのセキュリティ設定や各ローカル環境の構築手順準備、作業結果のとりまとめなどなど、、、すべてやる必要がなくなります。超絶べんり。 今回はプライベートグループを使用して、こんな感じで社内の人たちに有志を募って作業をお願いしました。 ※お手伝いいただいた方々、ありがとうございました! 依頼したときの画面 モデルをトレーニングして、推論のAPIを構築する ここからは実際にAmazon SageMakerのモデルトレーニング機能を使用して、モデルをトレーニングしていきます。 使用するAPI ちなみにSageMakerを扱うAPIは2種類存在し、どちらを使用いただいても問題ないようです。 boto3 API s3などの処理を行うのにおなじみのAPIです。以下のインスタンスを起動すると、 boto3 から sagemaker 用のランタイムを起動できます client = boto3.client(service_name='sagemaker') SageMaker API こちらがメジャーとなっているAPIで、基本的にはこちらを使用するようです。 今回は馴染みのある 『boto3 API』を使用しました。 ハイパーパラメーターを指定する モデルを作成するにあたって、すべてSageMakerのAPIにおまかせ!!ってわけではなく、ある程度モデルに対するパラメーターをこちらで指定する必要があります。これは使用するモデルのアルゴリズムによって異なるので今回の記載は割愛しますが、詳細を見たい方は以下を参照ください。 オブジェクト検出アルゴリズムのハイパーパラメーター ちなみに今回指定したパラメーターは以下の通り training_params = \ { "AlgorithmSpecification": { "TrainingImage": training_image, "TrainingInputMode": "Pipe" }, "RoleArn": role, "OutputDataConfig": { "S3OutputPath": s3_output_path }, "ResourceConfig": { "InstanceCount": 1, "InstanceType": "ml.p3.2xlarge", "VolumeSizeInGB": 50 }, "TrainingJobName": job_name, "HyperParameters": { "base_network": "resnet-50", "use_pretrained_model": "1", "num_classes": "4", "mini_batch_size": "16", "epochs": "50", "learning_rate": "0.001", "lr_scheduler_step": "3,6", "lr_scheduler_factor": "0.1", "optimizer": "rmsprop", "momentum": "0.9", "weight_decay": "0.0005", "overlap_threshold": "0.5", "nms_threshold": "0.45", "image_shape": "300", "label_width": "350", "num_training_samples": str(num_training_samples) }, "StoppingCondition": { "MaxRuntimeInSeconds": 86400 }, "InputDataConfig": [ { "ChannelName": "train", "DataSource": { "S3DataSource": { "S3DataType": "AugmentedManifestFile", "S3Uri": s3_train_data_path, "S3DataDistributionType": "FullyReplicated", "AttributeNames": attribute_names } }, "ContentType": "application/x-recordio", "RecordWrapperType": "RecordIO", "CompressionType": "None" }, { "ChannelName": "validation", "DataSource": { "S3DataSource": { "S3DataType": "AugmentedManifestFile", # NB. Augmented Manifest "S3Uri": s3_validation_data_path, "S3DataDistributionType": "FullyReplicated", "AttributeNames": attribute_names # NB. This must correspond to the JSON field names in your augmented manifest. } }, "ContentType": "application/x-recordio", "RecordWrapperType": "RecordIO", "CompressionType": "None" } ] } トレーニングを実行 ハイパーパラメーターを定義したら、実際に実行してみます! client.create_training_job(**training_params) モデルをデプロイし、コンテナ上で推論を動かす SageMakerのすごいところは、予め用意された 組み込みアルゴリズム 以外にも、作成されたモデルを1ボタンで簡単にデプロイして、推論APIまでの構築を行ってくれることです。今までエンドポイント構築に苦労してきたデータサイエンティストにとっては、これからはモデルの作成や調整に時間をかけることができるというすぐれものです!! 実際にエンドポイントの設定を行い、 from time import gmtime, strftime import time import sagemaker as sage sage = boto3.Session().client(service_name='sagemaker') timestamp = time.strftime('-%Y-%m-%d-%H-%M-%S', time.gmtime()) endpoint_config_name = 'apt-snacks-endpoint-config' endpoint_config_response = sage.create_endpoint_config( EndpointConfigName = endpoint_config_name, ProductionVariants=[{ 'InstanceType':'ml.m4.xlarge', 'InitialInstanceCount':1, 'ModelName':'apt-snacks-model', 'AcceleratorType': 'ml.eia1.xlarge', 'VariantName':'AllTraffic'}]) エンドポイントをたてます。 endpoint_params = { 'EndpointName': endpoint_name, 'EndpointConfigName': endpoint_config_name, } endpoint_response = sage.create_endpoint(**endpoint_params) エンドポイントがたったことを確認したら、テスト用に推論APIを読み出すスクリプトを簡単に書き、実行してみます。 session = boto3.Session(region_name='us-east-2') runtime = session.client('sagemaker-runtime', config=config) #SageMakerのエンドポイント名の指定 endpoint_name = 'apt-snacks-endpoint' response = runtime.invoke_endpoint( EndpointName=endpoint_name, ContentType='image/jpeg', Body=payload ) result = json.loads(response['Body'].read().decode()) そして返ってきたデータを元に、Bounding Boxを出力してみると・・・ 検出結果たち いい感じに実行できました!(一部誤検出していますが。。) ものすごく簡単!!! intdashとSageMakerの仲介役となるスクリプトを作り、intdash Analytics Servicesにデプロイする 上記にて作成したスクリプトを拡張して、以下の処理を行うスクリプトを作成します。 raspiに導入している intdash edge から送られてきた画像データを取得する SageMakerの推論APIに画像データを送付し、推論結果を取得する 推論結果を可視化し、 intdash に アップロードする 作成したコードは、実際に動かすための実行環境である 『intdash Analytics Services』にデプロイします。 動かしてみる 上記を構築した上で、raspiから動画をストリーミングすると以下のように動かすことができます! Real-time object detection with Amazon SageMaker & intdash Services まとめ いかがでしたでしょうか。 今回はintdashとAmazon SageMakerを使ってお菓子のリアルタイム検出システムを簡単に構築することができました。難しいイメージの機械学習でも、SageMakerのAPIを使うことで簡単にモデルの作成からエンドポイントの構築まで自分で作成することができます。そこに弊社の高速データストリーミングプラットフォームと組み合わせることで、検出結果を動画の撮影と同時にリアルタイムで可視化でき、開発者側はインフラストラクチャーレベルを考慮せずシステムの構築を行うことができました。この記事を通して、Amazon SageMakerと弊社製品のintdashに少しでも興味をもっていただければ幸いです。 ※宣伝です。 ちなみにaptpodではデータサイエンティストを絶賛採用中です!もし本記事を見て、 『intdashと機械学習の技術を使ってサービス作ってみたい!』 など思ってくれる方がいらっしゃいましたら、ぜひ以下のHPまでどうぞ! 一緒に世界レベルの高速データ解析の実現を目指していきましょう! 採用情報: https://www.aptpod.co.jp/recruit/
先端技術調査グループの酒井です。 今年も Aptpod Advent Calendar 2019 に参加することになり 1日目を担当することになりました。 ちょっと前の話になってしまいますが、2019/12/2から始まるAWSのイベント 『re:Invent 2019』 へ展示していることもあり、AWS RoboMaker 関連の取り組みをお送りします。 AWS RoboMaker(ロボット工学アプリケーションの開発、テスト、デプロイ)| AWS AWS RoboMakerを使って、TurtleBot3 with OpenMANIPULATORに自社製品intdashを組み込んで、インターネット経由での遠隔制御に挑戦しました! 成果物は 2019/6/12〜14に開催された AWS Summit Tokyo 2019 で展示しました。 本記事ではその取り組みの中で、できたこと、できなかったこと、今後の展望などをまとめています。 ただ、AWS RoboMakerがTokyo Regionで提供される直前くらいからおおむね2か月くらいの取り組みだったので、できなかったことや積み残しも結構残っています。 できたこと AWS RoboMakerと自社製品のintdash Edge Moduleを使って、インターネット経由でTurtleBot3を遠隔制御できるようにしました。 具体的には、 PS3コントローラーを使って、TurtleBot3の実機とシミュレーション環境(Gazebo) 内のTurtleBot3を操作する 実機とシミュレーション両方を同時に操作する ことができます。 実際の動画 実際に動かしている様子を動画で撮影しました。 一つ目の動画は、コントローラーで操作している動画です。 Remote control TurtleBot3 with AWS RoboMaker+intdash 二つ目の動画は、シミュレーション環境との同時操作とその結果を可視化している動画です。 Visualize remote control TurtleBot3 with AWS RoboMaker+intdash 取り組みの背景 AWS RoboMakerと自社製品intdashでコラボレーションしたものを何か展示できないかというのがきっかけでした。 弊社としては、intdashがRoboMakerで使えると、パッケージとしての提供やデプロイが可能になるので、自社のデプロイの工数を減らすことができる可能性があります。 また弊社の製品を提供するという視点だと、 AWS RoboMakerを使うことによってROSの環境構築が簡単にできる点 Amazon SageMakerなどのAWSのアセットとの連携がしやすくなるという点 がメリットとして考えられそうです。 ビジネス的な価値はまだ明確にできていないので、今後も検討したいと思います。 使用したロボット、環境、自社製品の紹介 TurtleBot3 with OpenMANIPULATOR ロボットは、Turtlebot3 (with waffle pi) というロボットにOpenMANIPULATORというアームを取り付けた TurtleBot3 with OpenMANIPULATOR を使用しました。 Turtlebot3は研究、教育、プロダクトのプロトタイピングなどに使われるロボットです。ハードウェア、ファームウェア、ソフトウェアのすべてがオープンソースで開発されています。ROS界隈ではよく使われています。また、すぐに使える色々なパッケージが提供されています。例えば、Bluetoothで接続したコントローラーを使った操作、SLAMを使った地図作成、自己位置推定、Navigationを行うためのROSのパッケージが提供されています。 詳しい情報は、 Robotis のサイトで見ることができます。 Open Manipulatorはロボットアームです。こちらもTurtlebot3と同様、ハードウェア、ファームウェア、ソフトウェアがオープンソースで開発されています。コントローラーを使った操作、軌道を生成して軌道通りにアームを動かすためのパッケージなどが提供されています。こちらも、 Robotis のサイトに詳しい説明が載っています。 TurtleBot3とOpenMANIPULATORを組み合わせた時に必要となるファームウェアやソフトウェアなどもオープンソースで提供されています。 実際に組み立てた完成品は以下のようになります。 Turtlebot3 with OpenManipulator 完成品 AWS RoboMakerの紹介 AWS RoboMaker は、AWSが提供するロボットアプリケーションの開発、シミュレーション(テスト)、デプロイが行えるサービスです。 開発 Cloud9を使って、ROSアプリケーションの開発ができます。Cloud9はクラウド上で開発ができる統合開発環境です。AWS RoboMakerでは、Cloud9にROSアプリケーション開発用の拡張がされています。それによって、ビルド、バンドル、シミュレーションジョブの起動までの処理をすべてCloud9 上から行うことができます。 Cloud9 利用画面 ※バンドルとは、ROSアプリケーションの実行時に依存するライブラリ群をすべてひとまとめにパッケージ化する処理のことです。必要なライブラリがすべてまとまっているので、デプロイ先ではライブラリの依存関係などに悩まされることを無くすための仕組みです。バンドルの詳細は 公式のこちら で確認することができます。 シミュレーション(テスト) 実装したROSアプリケーションをGazebo、rqt、Rvizなどを使ってシミュレーションする環境がされています。またターミナルも用意されているのでコマンドラインを使うことも可能です。 シミュレーションジョブ利用画面 シミュレーションには、turtlebot3_gazeboなどのシミュレーション環境上で必要なROSパッケージをまとめたSimulation Applicationと、(最終的に)実機にデプロイして使うROSパッケージをまとめたロボットアプリケーションをそれぞれバンドルしたものを使います。 下の画像は実際にシミュレーション環境でTurtleBot3 with OpenMANIPULATOR を動かした画面をキャプチャしたものになります。 シミュレーションジョブ上でGazeboを利用してTurtletbo3 with OpenManipulator 動かした例 デプロイ AWS IoT Greengrassを使って、ロボットアプリケーションを実際の機体にデプロイできます。 ※事前にTurtleBot3のRaspberry Pi上でGreengrassの設定が必要です。 デプロイを行うと、バンドルしたROSアプリケーションがGreengrass経由でTurtleBot3のRaspberry Piにダウンロード、展開されます。それらを使ってROSアプリケーションを実機上で起動します。 実際には↓のような画面からデプロイの設定を行います。 デプロイ画面 intdashとは? 自社で開発を行っている製品です。 intdashは、100ミリ秒∼1ミリ秒間隔程度の高頻度で発生する時系列データを品質保証のないネットワークを経由して、高速・大容量かつ安定的にストリーミングするための双方向データ伝送プラットフォームです。 intdashは、プラットフォームを構成する製品・サービスの総称(ブランドネーム)を兼ねており、 INTeractive DAta Streaming Hub の頭文字を並べた略称です。 https://www.aptpod.co.jp/basetech/ より エッジデバイスから時系列のデータを収集する機構、サーバ側で時系列のデータを保存する機構、UIで時系列のデータを可視化する機構などを備えた製品になります。 双方向のデータのやり取りができるため、本記事のようにロボットを使用する場合は、モニタリングや遠隔制御に使うことができます。自社製品のintdash Edge ModuleはRaspberry Pi 上でも使えるので、今回はそれを使ってデータを流しています。 どうやって動かしたのか 操作側は、PS3コントローラーにRaspberry Pi を接続したものを用意しました。操作情報を送信する側と、Turtlebot3側にそれぞれintdash Edge Moduleを組み込みます。これにより、intdash Edge Moduleはrosbridge経由でPS3コントローラー側の操作情報を、TurtleBot3に渡すことが可能になります。 ROSはローカルネットワークで使うことを前提としています。調べた限りでは、インターネット経由でメッセージのやり取りをする標準的な方法はなさそうでした。なので、インターネットを経由して操作情報を渡すために自社製品のintdash Edge Moduleを使いました。 全体の構成 これによってPS3コントローラーから吸い上げた操作情報をTurtleBot3に届けて、遠隔で制御することができるようになりました。 データの流れ ここから実際どういうメッセージが流れているかを説明します。 PS3コントローラーの制御情報は以下の図のように、 PS3コントローラー → PS3コントローラー用Raspberry PiのROSノード → intdash Edge Module → インターネット → クラウド上のサーバ → インターネット → intdash Edge Module → turtlebot3用Raspberry PiのROSノード → OpenCR と伝達されます。 その結果をもとにdynamixel (モーター)を制御します。 データの流れ 具体的な制御メッセージは以下のように流れます。 PS3コントローラーからBluetoothでRaspberry Piに制御信号を飛ばします。PS3コントローラー側内部のROSのノードで信号を拾い sensor_msgs/Joy 型の /teleop/joy メッセージに変換します。 変換された /teleop/joy は、PS3コントローラー用Raspberry Pi上で動いているintdash Edge Moduleに渡されます。 PS3コントローラー側のintdash Edge ModuleからTurtleBot3側のintadsh-edgeまで自社製品経由で、 /teleop/joy メッセージを送ります。PS3コントローラー側のintdash Edge Moduleがpublishした /teleop/joy メッセージを、turtlebot3側のintdash Edge Module がsubscribeする形になっています。 turtlebot3側のintdash Edge ModuleはTurtleBot3用Raspberry Piの /rosbridge_tcp に /teleop/joy メッセージを渡します。 /rosbridge_tcp  は、 /teleop/joy メッセージをpublishします。これによって、インターネット経由で取得したPS3コントローラーによる操作情報が無事に、ROSのネットワーク内部まで到達しました。 /om_with_tb3/teleop_twist_joy が sensor_msgs/Joy 型の teleop/joy メッセージを受信します。 /om_with_tb3/teleop_twist_joy は /teleop/joy メッセージを、 geometry_msgs/Twist 型に変換して、 /om_with_tb3/cmd_vel をpublishします。 その後、 /om_with_tb3/cmd_vel をOpenCR内部にいるROSノードが受け取って、TurtleBot3の車輪の制御をしているdynamixelを制御します。ちなみに、OpenCR上でもROSノードが動いていて、 /om_with_tb3/cmd_vel を subscribe しています。また、シミュレーション環境では、 /gazebo/om_with_tb3/cmd_vel を受信します。 実現できなかったこと アームを動かす Turtlebot3に装着したOpenMANIPULATORを遠隔制御で動かすのは、今回のAWS Summit Tokyo 2019までの準備期間では間に合いませんでした。Gripperは動かすことができましたが関節は動かせませんでした・・・。 できなかった理由 原因として考えているのは以下の2点です。 Turtlebot3にOpenMANIPULATORを装着した状態で、OpenMANIPULATORをコントローラーで動かす方法が見つけられなかった点 一般的にはMoveIt経由で動かします。(参考: http://emanual.robotis.com/docs/en/platform/turtlebot3/manipulation/ ) OpenMANIPULATOR単体をコントローラーで動かす方法はあります。しかし、TurtleBot3と結合した状態のOpenMANIPULATORをコントローラーで動かす方法は探した限りでは見つかりませんでした。 ROSのノードの構成が、本来TurtleBot3で想定している構成と差異がある点 上記と関連しますが、 /arm/moveit は本来remote PCで動かすことを想定しています。 Raspberry Pi上で /arm/moveit を動かそうとしたが、クラッシュしてしまいました。ログを見た限りではメモリ確保の処理で落ちている挙動でした。Raspberry PiではRaspbianを使っていたので、元々64bit向けに実装されていた部分があったとすると、それを32bitで動作させようとしたことが原因になっているかもしれません。 データ吸い上げ シミュレーション環境では実現できましたが、実機ではRaspberry Piの性能の問題で遠隔制御との両立ができませんでした。 詳細な調査はしていませんが、原因はパフォーマンスの問題の可能性が高いと考えます。内部のプロセスを htop コマンドで確認したところ、rosbridge_serverのCPU使用率がほぼ100%に張り付いている状態でした。 rosbridge_serverの負荷が高かった原因は、TurtleBot3内部でodometry, imuがかなり高頻度でpublishされていた点と、PS3コントローラーからのメッセージもかなりの高頻度だった2点だと予想しています。 カメラを動かす raspi-cameraは依存関係上バンドルされていなかったライブラリがあったのが原因で動いていませんでした。 今後試したいこと rosbridgeに渡すデータ量を減らす odometry、imuといった情報がかなりの高頻度でpublishされています。rosbridgeに渡す前にトピックの量を間引くことで、rosbridgeの負荷を減らすことはできると思います。PS3コントローラー側から出しているトピックも今はかなりの高頻度で出力しているので、これも操作に支障がないレベルまで減らすことができそうです。 トピック量を減らすことで、モニタリングと遠隔制御が両立できるとよさそうです。 OpenMANIPULATORを動かす。 Raspberry Pi自体はもともと64bitのアーキテクチャなので、Raspbianを別なOSに変えることも検討したいと思います。 Raspberry Pi置き換え。 Raspberry PiをJetsonなどのほかの組み込みボードへの置き換えが考えられます。Raspberry PiとOpenCRは、USB経由でシリアル接続をしています。だから、OpenCRとシリアル接続ができれば問題なく使えるのではと考えています。 これにより、CPUの処理負荷の問題は解決できると考えています。どのOSを使うかにもよりますが、64bitを想定した実装を32bit向けにビルドしている問題は解決も一緒に解決できる可能性があります。 Amazon SageMakerとの連携 完全に妄想ですが、集めたデータを、 Amazon SageMaker を使って機械学習したり、そこから制御コマンド発行できたりする連携が可能になると面白いと思います。 ビジネス的な価値の検討 弊社の製品をAWS RoboMaker、あるいはAmazon SageMakerなどと組み合わせてどういう価値が生み出せるのか、まだまだ掘り切れてないところがあるのでそれを明確にする活動は今後も続けたいと思います。 AWS RoboMakerを使ってみて課題に感じた点 ROS Masterの構成の違い 一般的に使われているTurltleBot3とROSのネットワークの構成が違うのが課題に感じました。 調べた限りでは、TurltleBot3はもともとローカルネットワークで動くことを想定しています。AWS RoboMakerを使用しない場合は、以下の図のようにPCとRaspberry Pi上にROSのノードが存在しています。 Turtlebot3のROSノードの構成 これによって、PCとturtlebot3のRaspberry Piで動作が分散されます。例えば、 SLAM はPCで動かすことが想定されています。またこのような構成の場合、ROS MasterはPC側で動かすのが普通なのではないかと個人的には思います。 一方、AWS RoboMakerを使用した場合、Greengrass経由でデプロイされるので、ROS Masterを含むすべてのROSノードがraspberry pi上で実行されます。そのため、Raspberry Piの負荷が高くなる可能性が高いです。 RoboMaker使用時のROSノードの構成 詳細な調査は行っていませんが、今回の構成でSLAMなどをRaspberry Pi側で実行するのは負荷的に厳しかったです。 確認はしていないですが、AWS RoboMaker経由でデプロイした場合でも、TurtleBot3と同じネットワーク上にいるPCを使って処理の分散はできるかもしれないです。 また、intdashを使うことで、PC上との分散処理が実現できる可能性もあると思います。 感想 私は今回がROSを扱うのが初めてでした。4月入社でAWS Summit Tokyoまではおおむね2か月ほどの開発期間でしたが、ひとまず動くものができてよかったです。また、AWS RoboMakerを使うことで、開発環境の構築は苦戦することなく、ROSの使い方を覚えて開発を進めることに集中できたのでよかったです。 技術的には、ロボットの物理的な組み立て、OpenCRを使ったファームウェア、Raspberry Pi、AWS上での開発、ROSの開発など短期間で集中的にいろいろなことができて楽しかったです。手探りの状態で少しずつ進めないといけなかった点はとても大変でした。 開発では、バンドルの処理時間が重かった点で苦労しました。バンドル処理は並列処理されずにCPU1コアで処理されます。処理にかかる時間はバンドルするライブラリやパッケージの量により変わります。私が試していたときは、10~20分くらいバンドルに処理がかかることがありました。実機へのデプロイ時には、デプロイしたファイルの展開にも10から20分くらい時間がかかることがありました。これによって、わからないことや動作を実機上で確認する試行錯誤のための待ち時間がかなり長くなってしまった点が大変でした。 この点やほかにもAWS RoboMakerを使っていて苦労した点などは、AWS様にフィードバックをする機会がありましたので、今後の改善に期待しています。 そういった中で驚いた点は、AWS RoboMakerの改善の早さです。 具体的に例を一つ上げます。当初はデプロイ先が /tmp でしたが、6/25辺りにgreengrass user直下のディレクトリに変更されました。当初はデプロイ先が /tmp だったため電源を落とすとすべてデータが消えてしまいました。greengrass user直下にデプロイ先が変更されたことによって、電源を切るごとにバンドルされたファイルをダウンロードして展開する処理が必要なくなりました。 また、私が開発している期間の中で、シミュレーション環境のダッシュボードの改善なども行われていました。 最初にリリースした段階でAWS RoboMakerのコアの機能は実装されており、そののちにユーザーの利便性のために改善していくという点がMVP(Minimum Viable Product)の考え方に近い感じがしてとても参考になりました。 以上になります。 最後まで読んでいただきありがとうございました! おまけ1 本記事で扱ったAWS Summit 2019 Tokyoへ向けた取り組みを2019/11/13に 『IoT@Loft #5 - クラウドとロボティクス、オープンソース活用による次世代ロボットの可能性』 にて「AWS RoboMakerと連携するクラウド経由の遠隔制御の取り組み」というタイトルで発表させていただきました。 おまけ2 2019/12/2~12/6に米国ラスベガスで開催されるAWSのイベント 『re:Invent 2019』 にも展示します! AWS Summit Tokyo 2019での展示よりも進化しています!続編に期待!?
はじめに CTOの梶田です。 弊社も技術ブログを始めることにしました! アプトポッドは、現在 オートモーティブ 産業モビリティ(重機、建機、農機、ロジスティクス) ロボティクス を中心とした産業向けIoTプラットフォーム事業を展開しています。 会社全体としてエンジニアもしくはエンジニア出身の割合が8割程度と多く、多様なエンジニアが在籍しているなか、様々な取り組みを行っています。 実際どんなことをやってるのかよくわからないという声もあり、 技術調査や製品/研究開発における取り組み、エンジニア組織への取り組み、イベント出展における技術的なフォロー等々、技術やエンジニアリングにフォーカスした内容を発信していきたいと思っています。 初回としては、弊社について知っていただくためにエンジニア組織の概要を薄〜く広い内容でお届けします。 エンジニア組織の概要 背景 IoTのテクノロジーは、複数の専門分野をまたがる、総合格闘技のような側面があり、 1社で全ての技術をカバーするのは難しいとされています。 そういった中でアプトポッドは、 ハードウェア〜組み込みソフト〜サーバー〜アプリケーションまで一貫した開発を強みとしており、 レイヤー間を跨いだ総合的な課題解決ができる組織であると考えています。 ※ちなみにデザインも自社でやってます!!けっこう好評です! 組織 エンジニア組織は、様々な専門領域のレイヤーのエンジニアで構成されており、大きく以下の3つの開発を行っています。 製品開発 案件開発 研究開発 専門領域のレイヤーのエンジニアとしては、現状以下のように多種多様です。 ハードウェア 組み込みソフトウェア サーバー インフラ / ネットワーク フロントエンド(Webアプリ) モバイルアプリ(iOS、Android) Windowsアプリ QA(品質保証) 機械学習 その他プロトコルやロボティクス等々、いろんな話題があります。 まだまだそんな大きな組織ではないので小さなチームの連合体として日々戦っている状況です。 おまけ:技術スタック 詳細はまた別の機会に。。。 最後に このあと弊社エンジニアによって様々な取り組みを発信していきますので乞うご期待! 早速ですが、このあとすぐ12月から今年もQiita Advent Calendar 2019に参加し、今年はこの技術ブログを活用していきます。 aptpod Advent Calendar 2019 まだまだやりたいこともやれてないこともたくさんあり、エンジニアは絶賛募集中です! このブログを発信していく中で興味があるエンジニアの方はぜひぜひご応募お願いします! 採用情報: https://www.aptpod.co.jp/recruit/