Azarashi Tech Blog

日常における日常的なことやテクノロジー的なこと

Windows 環境でROS2で動くLivox LiDARのデータ取得プログラムを公開してみた。

背景

  • DJIが売ってるLivox というLiDARがある。かなり高精度で遠くまで見れて、しかもリーズナブル。ロボティクス関係で知っている人は多いのではないか。

www.livoxtech.com

  • プライベートで知り合いがLivox AVIAというのを持っていて、それをWindowsで使えるようにしたいという話がありました。
  • ROSでやりたいなぁと思ったが、WindowsでROSを使えないのは面倒だなぁと思ったのと、ROS2触ったことがないので触ってみようと思い、ちょっとROS2対応かつWindowsで動くLivox LiDARのハンドリングプログラムを作ってみようと思いました。
  • 土日にちょっと作った程度なのでクオリティはそこそこ。

コード/仕様

  • こちら。あんまりgithubにコード上げてこなかったので、レポジトリがなくてカスッカスなのがバレて恥ずかしいですが。

github.com

  • ベースとしてとりあえずLivox SDKがあって、それをいじってROS2対応させただけです。
  • あと本当はrviz2で表示するときに反射強度属性(intensity)に反射強度値を入れたかったが、なぜかうまくいかず、一旦RGB値に反射強度を入れて終わらせている。直したいけど、もうLivox AVIAを知り合いに返してしまったので改善はできない。残念。

まとめ

  • すっごい雑で申し訳ないのですが、作ったROS2で動くWindowsのLivox LiDAR用のデータ取得プログラムを作ったので、紹介しました。もし仕事やプライベートでLivoxのLiDARを使いつつWindowsで使いたくてROS2でなんとかしたい人がいれば活用してみてください。やってみて思いましたが、Linuxの方が圧倒的に楽ですね。
  • すっごい久しぶりに更新しましたが、また更新できる範囲で更新していきたいです。

ros3djsの仕組みと使い方を調べてみた

今回は、ダーツスキル評価システム関係なしに、ROSという超有名なRobotics向けのフレームワークに関する記事です。 とはいっても、実はダーツスキル評価システムにもROSを使っています。

ROSとは?

  • これを説明していると記事を何枚書いても終わらないので、ドキュメントのリンクだけ貼っておきます。以降、ROSを知っているものとして話を進めます。

    wiki.ros.org

  • 最近では、徐々にROS2への移行が進んでいます。

    index.ros.org

  • 本記事とは関係ないですが、ROSを勉強したい場合はROSのページにあるチュートリアル(すごく充実している)か、Robot Ignite Academyで勉強するのがおすすめです。

    www.theconstructsim.com

  • 本記事はROS2ではなくROSのパッケージの1つである、ros3djsに関してです。

背景

  • ROSには、ROS上で動くロボットシステムの状態可視化をうまく行えるrvizという優秀なツールが有ります。ロボットシステムのデバッグをしたいとき、デモをしたいときなどは非常に便利です。

    wiki.ros.org

  • しかし、時にロボットシステムと直結しているパソコン上でrvizを使うだけではなく、iPad等でリモートでブラウザ経由でrvizのような表示を見たくなるときがあります。そういう表示を自分で作るのは非常に大変です。 そこで既存のROSパッケージで活用できる良いのがないか調べたところ、ヒットしたのがros3djsでした。

  • ros3djsは実際使ってみて良さそう!と思ったのですが、中身が全然ぱっと見わからないし、日本人で調べている人があまりいなそうだったので、記事を書いてみることにしました。

ros3djs

  • 端的に言えば、ROSで扱っているデータを、ブラウザ上でrvizのように3D表示できるようにするツールです。Webサーバー上で動かせば、他のパソコンからブラウザ経由で同じ表示を見れます。
  • BSDライセンスです。

    wiki.ros.org

  • Robot Web Toolという「BRINGING ROBOTS TO YOUR FAVORITE BROWSER」というテーマで色々とROSとブラウザを繋げるプロジェクトの一貫として作られているもののようです。

    robotwebtools.org

  • 動作している様子の動画。

ros3djsのバックエンドはどうなってるか?

  • ROS Wikiによると、「roslibjsの上に作られており、three.jsの力を使っている」と書かれている。

    It is build ontop of roslibjs and utilizes the power of three.js.

three.js

roslibjs

  • ROS Wikiによると、「ブラウザからROSへのインタラクションを行うためのコアJavascript Library」。

    roslibjs is the core JavaScript library for interacting with ROS from the browser

  • WebSocketsを用いてrosbridgeと接続し、publishing(配信処理), subscribing(購読処理)、サービスコール、actionlib, TF, URDFパーシングなどの重要なROS機能を提供。
  • これもRobot Web Toolの一貫で作られたもの。

    wiki.ros.org

  • 一応用語説明・・・

    • WebSocket

      Webにおいて双方向通信を低コストで行う為の仕組み(プロトコル)。  qiita.com

    • rosbridge

      ROS非対応のプログラムから、JSON API を使ってROS機能を使えるようにするモジュール。 Webブラウザ向けのWebSocketサーバなど、rosbridgeへのインターフェースとなるフロントエンドは他種類用意している。 wiki.ros.org

関係性をざっくり図でまとめると・・・

  • 下図のようになると考えられる。

f:id:surumetic-machine-83:20190119163128p:plain

使い方

インストール

  • githubからgit cloneして、README.mdのinstallするだけ。自分の場合は、下記のような感じ。
cd (ros3djsをインストールしたいディレクトリ)
git clone https://github.com/RobotWebTools/ros3djs.git
cd ros3djs
sudo apt-get install nodejs nodejs-legacy npm
sudo npm install -g grunt-cli
sudo rm -rf ~/.npm ~/tmp
npm install .

サンプル実行

  • 一番わかり易いサンプルは、ros3djs/examplesの中にある「markers.html」なので、これを実行する。
  • まず下記コマンドでrosbridge-serverとtf2-web-republisherをインストール
sudo apt-get install ros-kinetic-tf2-web-republisher ros-kinetic-rosbridge-server
  • 次に下記コマンドをそれぞれ別terminalで実行。つまり4つterminal必要。
roscore
rosrun visualization_marker_tutorials basic_shapes
rosrun tf2_web_republisher tf2_web_republisher
roslaunch rosbridge_server rosbridge_websocket.launch
  • 最後にmarker.htmlを開くと、描画が見れる。マウスで自由に視点変えられる。

f:id:surumetic-machine-83:20190119192535p:plain

どうすれば独自のマーカーを表示できるようになるか

  • ぶっちゃけ、このSimple Marker Exampleでマーカーtopic表示できるようになったのだから、トピック編集して中身変えれば何でも表示できる。htmlファイルの中身見て何をやっているか見れば良い(はず)。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />

<script src="https://static.robotwebtools.org/threejs/current/three.js"></script>
<script src="https://static.robotwebtools.org/EventEmitter2/current/eventemitter2.min.js"></script>
<script src="https://static.robotwebtools.org/roslibjs/current/roslib.js"></script>
<script src="../build/ros3d.js"></script>

<script>
  /**
   * Setup all visualization elements when the page is loaded. 
   */
  function init() {
    // Connect to ROS.
    var ros = new ROSLIB.Ros({
      url : 'ws://localhost:9090'
    });
    // Create the main viewer.
    var viewer = new ROS3D.Viewer({
      divID : 'markers',
      width : 400,
      height : 300,
      antialias : true
    });
    // Setup a client to listen to TFs.
    var tfClient = new ROSLIB.TFClient({
      ros : ros,
      angularThres : 0.01,
      transThres : 0.01,
      rate : 10.0,
      fixedFrame : '/my_frame'
    });
    // Setup the marker client.
    var markerClient = new ROS3D.MarkerClient({
      ros : ros,
      tfClient : tfClient,
      topic : '/visualization_marker',
      rootObject : viewer.scene
    });
  }
</script>
</head>

<body onload="init()">
  <h1>Simple Marker Example</h1>
  <p>Run the following commands in the terminal then refresh this page.</p>
  <ol>
    <li><tt>roscore</tt></li>
    <li><tt>rosrun visualization_marker_tutorials basic_shapes</tt></li>
    <li><tt>rosrun tf2_web_republisher tf2_web_republisher</tt></li>
    <li><tt>roslaunch rosbridge_server rosbridge_websocket.launch</tt></li>
  </ol>
  <div id="markers"></div>
</body>
</html>
  • 結局、init関数の中にros3djsで表示する内容を定義しているに過ぎない。TFツリーとトピック名指定して描画設定できる。ビューアはidを設定した後、divタグでid指定すれば、好きな場所にビューアを表示できる(はず)。
  • APIの詳しい説明は、下記参照。

    http://robotwebtools.org/jsdoc/ros3djs/current/index.html

まとめ

  • ros3djsの仕組みと使い方をざっくり書いてみた。
  • とは言いつつ、自分もまだそこまでフル活用できていない。どんどん使って理解深めたい。
  • どのくらい有名なのかわからないが、意外とすごいツールな気がする。
  • 2013年くらいからあるっぽいけど、今日見た時点でgithubも3日前にコード変更されてたし、ちゃんとメンテされてるっぽい。

スキル評価系論文の調査(1)

背景

  • 今一度、ダーツ投擲フォーム評価システムに使う手法の選定のために論文調査する。
  • 時系列入力からスキル評価をする論文をざっくり見てみる。
  • 4つだけだけど、すべて2017年と2018年。

論文

Who’s Better? Who’s Best? Pairwise Deep Ranking for Skill Determination

arxiv.org

  • CVPR2018
  • 対評価で上手下手をランキングしていく感じで、手術・ピザ生地作り・箸の使い方・絵の描き方のスキル評価をしている。
  • ECCV2016のTSNを使っていて、つまりはRNN、LSTMとかは使ってないように見える。
  • Skill Determination from Egocentric Video(EPIC2017)の続きかな?

f:id:surumetic-machine-83:20190116064416p:plain

EvaluationNet: Can Human Skill be Evaluated by Deep Networks?

arxiv.org

  • S.T. Kim and Y. M. Ro. Evaluationnet: Can human skill be evaluated by deep networks? arXiv preprint arXiv:1705.11077, 2017.
  • Youtubeに上がってるインストラクションビデオ(つまりエキスパート)とユーザビデオ(つまり素人)の動画を入力して、上手いか下手か(Success or Fail)を判定。
  • いずれのビデオ入力も、まずはAction unit modelingという処理で、Visual Feature ExtractionとLSTM使ったエンコードが行われる。
  • 2つのビデオ入力がそれぞれエンコードされ、それらが統合されて特徴量ベクトルになったところで、2つのベクトルがSiameseネットワークで比較され、Success or Failという判定に入るというわけである。

f:id:surumetic-machine-83:20190116064433p:plain

The Pros and Cons: Rank-aware Temporal Attention for Skill Determination in Long Videos

arxiv.org

  • CoRR 2018
  • 長めの動画からスキル判定する手法。
  • 従来の手法は、ランダムにビデオを部分的にセグメントととしてたくさん切り取ってきて、ランキングをつけているが、これは適切ではないと考える。なぜならば、動画全体を通した「スキル」が多様に有り、一部のセクションからビデオの包括的なスキル判定などできないから。
  • 新しいrank-aware 損失関数というものを用い、video-level(=技能レベルと思われる)のみを教師として、rank-specific temporal attenstion module(ランキング専用の時間アテンションモジュールとでも訳すか?)というものを学習させる。
  • その新規に提案した損失関数は、同時にPros(長所)スキルとCons(短所)スキルを扱う2つの異なるアテンションモジュールを学習できるようにしている。
  • 世に公開されているEPIC-スキルデータセットでアプローチを評価し、さらに5つのこれまで未踏のタスクについて大きなデータセットを収集してアノテーションして学習データセットを作成。提案手法は、いずれのデータセットにおいても従来手法よりも良い成果を残した。ペアワイズ精度で4%、ここのタスクでは最大12%よかった。

f:id:surumetic-machine-83:20190116080620p:plain

Learning to Score Olympic Events

http://openaccess.thecvf.com/content_cvpr_2017_workshops/w2/papers/Parmar_Learning_to_Score_CVPR_2017_paper.pdf

  • CVPR2017
  • タイトル通り、オリンピックにおける行動の品質(競技スコア、難易度、等)をDeep Learningで推定できないか試した論文。 -競技は、ダイビング、跳躍、フィギュアスケート
  • C3D-SVR, C3D-LSTM および C3D-LSTM-SVRといった3つのフレームワークで評価。
    • C3Dと言ってるのは、3D Convolutional Neural Network
    • SVRよりLSTMベースのほうがよい性能だった。
  • 当時、「行動認識」とは異なり、「行動の品質」のデータセットは少なかったので、少ないデータセットからLSTMでうまく学習している。

f:id:surumetic-machine-83:20190116073206p:plain

所感

  • 有名ドコロの国際会議の論文は、ほとんど動画からスキル評価してる気がする。僕みたいな問題設定(画像認識は他のライブラリで済ませて、その出力を処理してるだけ)でやってる人らは、難しくない問題を扱ってると思われてハジかれてるのかな?(僕は趣味だからいいけど)
  • 次回は、この4つの論文のRelated Worksから、これまでのスキル評価系論文の歴史とトレンドを拾ってみようと思う。

Kerasで書いたDLモデルをOptunaでハイパーパラメータ最適化(1)

概要

  • ダーツスキル評価用のDLモデルのハイパーパラメータを最適化する。
  • 最適化には、Preferred Networks製Optunaを用いる。同社はChainerのメンテナーだけど、Optunaは別にchainer以外にも使える。今回はOptunaとKerasを合わせて使います。

ハイパーパラメータ最適化について

  • 概念的には、ここのページがすごくまとまっている。

    • 日本語の説明ページもネットには転がってるけど、だいたい説明がテキトーだから、こういうちゃんとした英語のページのほうを見たほうが良い気がします。
    • 気が向いたら全訳載せます。 towardsdatascience.com
  • 最近、ハイパーパラメータ最適化のライブラリは、Hyperopt, Optuna、Hyperopt、SMAC、MOE, Spearmintとか色々ある。

  • 上のページだとHyperopt推しだけど、2018/12/03にOptunaが公開されていて、そこではHyperoptとと同じくTPE(Tree-structured Parzen Estimator)というので計算できて、かつ「学習曲線を用いた試行の枝刈り」「並列分散最適化」といった点でより効率よく計算できるようになっている。詳しくは下記記事参照。

    research.preferred.jp

  • なお、TPEを使う場合は、「損失の上位グループと下位グループを分割する閾値y*」を設定する必要があるが、これはHyperoptと同じ設定にしていると書かれている。参考論文からすると「a quantile cutoff point of previous values」なので、単に過去の目的関数の評価結果値群の中央値をy*として選択しているのかなと思う。なのでユーザ側では設定不要。参考論文は下記。

    • 該当ソースコード読んだわけではないので、違ってたらごめんなさい。そして良ければ正しい情報教えてください。

    https://papers.nips.cc/paper/4443-algorithms-for-hyper-parameter-optimization.pdf

さっそくハイパーパラメータ最適化をやってみる

  • Optunaでハイパーパラメータチューニングしたい!どこから手をつけるべきか?というところだが、githubサンプルソースがあるので参考にする。
  • OptunaはPreferred Networks製なので、同社のライブラリであるchainerでサンプルが書かれている。

    github.com

基本的なハイパーパラメータ最適化コードの構成

  • だいたいどのハイパーパラメータ最適化も、基本構成はかわらない。
  • ハイパーパラメータチューニングは、結局のところ、目的関数の最小化だから、結局最適化関数と、その関数に入力する目的関数が定義されていればOK。
  • ざっくり、optunaライクに書けば例えば以下のような感じ。
def objective(trial):
    trial変数からのハイパーパラメータ候補の生成
    目的関数の定義
    return 目的関数の評価値

if __name__ == '__main__':
    study = optuna.create_study() # 最適化インスタンス作成
    study.optimize(objective, n_trials=トライアル回数)

 ハイパーパラメータ最適化結果の出力

サンプルソース

  • chainer_simple.pyが参考になる。

https://github.com/pfnet/optuna/blob/master/examples/chainer_simple.py


from __future__ import print_function

import chainer
import chainer.functions as F
import chainer.links as L
import numpy as np
import pkg_resources

if pkg_resources.parse_version(chainer.__version__) < pkg_resources.parse_version('4.0.0'):
    raise RuntimeError('Chainer>=4.0.0 is required for this example.')


N_TRAIN_EXAMPLES = 3000
N_TEST_EXAMPLES = 1000
BATCHSIZE = 128
EPOCH = 10


def create_model(trial):
    # We optimize the numbers of layers and their units.
    n_layers = trial.suggest_int('n_layers', 1, 3)

    layers = []
    for i in range(n_layers):
        n_units = int(trial.suggest_loguniform('n_units_l{}'.format(i), 4, 128))
        layers.append(L.Linear(None, n_units))
        layers.append(F.relu)
    layers.append(L.Linear(None, 10))

    return chainer.Sequential(*layers)


def create_optimizer(trial, model):
    # We optimize the choice of optimizers as well as their parameters.
    optimizer_name = trial.suggest_categorical('optimizer', ['Adam', 'MomentumSGD'])
    if optimizer_name == 'Adam':
        adam_alpha = trial.suggest_loguniform('adam_alpha', 1e-5, 1e-1)
        optimizer = chainer.optimizers.Adam(alpha=adam_alpha)
    else:
        momentum_sgd_lr = trial.suggest_loguniform('momentum_sgd_lr', 1e-5, 1e-1)
        optimizer = chainer.optimizers.MomentumSGD(lr=momentum_sgd_lr)

    weight_decay = trial.suggest_loguniform('weight_decay', 1e-10, 1e-3)
    optimizer.setup(model)
    optimizer.add_hook(chainer.optimizer.WeightDecay(weight_decay))
    return optimizer


# FYI: Objective functions can take additional arguments
# (https://optuna.readthedocs.io/en/stable/faq.html#objective-func-additional-args).
def objective(trial):
    # Model and optimizer
    model = L.Classifier(create_model(trial))
    optimizer = create_optimizer(trial, model)

    # Dataset
    rng = np.random.RandomState(0)
    train, test = chainer.datasets.get_mnist()
    train = chainer.datasets.SubDataset(
        train, 0, N_TRAIN_EXAMPLES, order=rng.permutation(len(train)))
    test = chainer.datasets.SubDataset(
        test, 0, N_TEST_EXAMPLES, order=rng.permutation(len(test)))
    train_iter = chainer.iterators.SerialIterator(train, BATCHSIZE)
    test_iter = chainer.iterators.SerialIterator(test, BATCHSIZE, repeat=False, shuffle=False)

    # Trainer
    updater = chainer.training.StandardUpdater(train_iter, optimizer)
    trainer = chainer.training.Trainer(updater, (EPOCH, 'epoch'))
    trainer.extend(chainer.training.extensions.Evaluator(test_iter, model))
    log_report_extension = chainer.training.extensions.LogReport(log_name=None)
    trainer.extend(chainer.training.extensions.PrintReport(
        ['epoch', 'main/loss', 'validation/main/loss',
         'main/accuracy', 'validation/main/accuracy']))
    trainer.extend(log_report_extension)

    # Run!
    trainer.run()

    # Set the user attributes such as loss and accuracy for train and validation sets
    log_last = log_report_extension.log[-1]
    for key, value in log_last.items():
        trial.set_user_attr(key, value)

    # Return the validation error
    val_err = 1.0 - log_report_extension.log[-1]['validation/main/accuracy']
    return val_err


if __name__ == '__main__':
    import optuna
    study = optuna.create_study()
    study.optimize(objective, n_trials=100)

    print('Number of finished trials: ', len(study.trials))

    print('Best trial:')
    trial = study.best_trial

    print('  Value: ', trial.value)

    print('  Params: ')
    for key, value in trial.params.items():
        print('    {}: {}'.format(key, value))

    print('  User attrs:')
    for key, value in trial.user_attrs.items():
        print('    {}: {}'.format(key, value))
  • 目的関数をobjective関数で定義。

    • モデルの定義をcreate_modelで、最適化手法の定義をcreate_optimizerで定義。
    • ハイパーパラメータの探索範囲は、trial.suggest_〜みたいな関数で値を振れるように書く。値が整数なのか、離散値なのか、浮動小数点なのか、広範囲の小数点なのか等で、suggestを変えてやる必要がある。 optuna.readthedocs.io
  • で、最後にstudy.optimizeで最適化。最適化した結果は、study.best_trialに格納される。

ダーツ評価モデルの場合

  • 僕の場合はKeras+Tensorflowでやっていたので、目的関数をkerasで書く。
  • OptimizerはSGD, Adam, Nadamを選べるようにした。
  • 層数、ノード数、ドロップアウト率、減衰率、学習率、等を変えてやった。
  • 目的関数は、validation lossとした。
import optuna
from keras.layers import Input, concatenate
from keras.layers.core import Activation, Flatten, Reshape, Dense, Dropout
from keras.layers.normalization import BatchNormalization
from keras.models import Model

import pandas as pd
import numpy as np
import glob
import os
import csv

import keras

count = 0

def create_model(trial):

    N_LAYERS_FINGER = trial.suggest_int('n_layers_finger', 1,10)
    N_LAYERS_BODY = trial.suggest_int('n_layers_body', 1,10)
    N_LAYERS_INTEGRATED = trial.suggest_int('n_layers_integrated', 1,10)

    finger_input_shape = (60, 6)
    body_input_shape = (60, 30)

    fingers_input = Input(shape=finger_input_shape)
    body_input = Input(shape=body_input_shape)


    x1 = Flatten()(fingers_input)
    for i in range(N_LAYERS_FINGER):
        n_units = int(trial.suggest_loguniform('n_units_finger_l{}'.format(i), 10, 400))
        drop_out_rate = trial.suggest_uniform('dropout_rate_finger_l{}'.format(i), 0.0, 1.0)
        x1 = Dense(n_units, name='finger_fc{}'.format(i))(x1)
        x1 = BatchNormalization()(x1)
        x1 = Activation('relu')(x1)
        x1 = Dropout(drop_out_rate)(x1)

    x2 = Flatten()(body_input)
    for i in range(N_LAYERS_BODY):
        n_units = int(trial.suggest_loguniform('n_units_body_l{}'.format(i), 10, 400))
        drop_out_rate = trial.suggest_uniform('dropout_rate_body_l{}'.format(i), 0.0, 1.0)
        x2 = Dense(n_units, name='body_fc{}'.format(i))(x2)
        x2 = BatchNormalization()(x2)
        x2 = Activation('relu')(x2)
        x2 = Dropout(drop_out_rate)(x2)

    x = concatenate([x1, x2])
    for i in range(N_LAYERS_INTEGRATED):
        n_units = int(trial.suggest_loguniform('n_units_integrated_l{}'.format(i), 10, 400))
        drop_out_rate = trial.suggest_uniform('dropout_rate_integrated_l{}'.format(i), 0.0, 1.0)
        x = Dense(n_units, name='integrated_fc{}'.format(i))(x)
        x = BatchNormalization()(x)
        x = Activation('relu')(x)
        x = Dropout(drop_out_rate)(x)

    n_units = int(trial.suggest_loguniform('n_units_integrated_l{}'.format(i), 5, 100))
    x = Dense(n_units, activation='relu', name='fc_final')(x)
    x = Dense(1, activation='sigmoid', name='fc_final_sigmoid')(x)

    model = Model(inputs=[fingers_input, body_input], outputs=x)

    return model


def create_optimizer(trial):
    optimizer_name = trial.suggest_categorical('optimizer', ['SGD', 'ADAM', 'NADAM'])

    if optimizer_name == 'SGD':
        sgd_lr = trial.suggest_loguniform('sgd_lr', 1e-5, 1e-2)
        opt = keras.optimizers.SGD(lr=sgd_lr, nesterov=True)
    elif optimizer_name == 'ADAM':
        adam_lr = trial.suggest_loguniform('adam_lr', 1e-6, 1e-2)
        weight_decay = trial.suggest_loguniform('weight_decay', 1e-10, 1e-3)
        opt = keras.optimizers.Adam(lr=adam_lr, decay=weight_decay)
    else:
        nadam_lr = trial.suggest_loguniform('nadam_lr', 1e-6, 1e-2)
        schedule_decay = trial.suggest_loguniform('schedule_decay', 1e-10, 1e-3)
        opt = keras.optimizers.Nadam(lr=nadam_lr, schedule_decay=schedule_decay)

    return opt


def objective(trial):
    global count
    count = count + 1
    print("progress : {}".format(count))

    # # ==========================================================================
    # #
    # # Set Model
    # #
    # # ==========================================================================

    model = create_model(trial)

    # # ==========================================================================
    # #
    # # Set Optimizer
    # #
    # # ==========================================================================

    opt = create_optimizer(trial)

    # # ==========================================================================
    # #
    # # Set Data Config
    # #
    # # ==========================================================================

    # center location vector for normalization
    center_location_norm_body = [1.8447725036231883, -0.17879784788302278, 0.3172548949275362]

    # scale value for normalization
    # assume person only moves 2.0 [m] at most
    scale_norm_body = 2.0

    scale_norm_finger_motion = 10000.0
    scale_norm_finger_pressure = 50000.0

    data_dir = "./*_log.csv"

    # ==========================================================================
    #
    # Load Data
    #
    # ==========================================================================

    X_fingers = np.empty((0, 60, 6), dtype='float32')
    X_body = np.empty((0, 60, 30), dtype='float32')

    y = np.empty((0, 1), dtype='float32')

    for files in glob.glob(data_dir):

        basename = os.path.basename(files)
        dir_name = os.path.dirname(files)

        # output_filename1 = dir_name + '/' + basename[:-4] + '_finger_normalized.csv'
        # output_filename2 = dir_name + '/' + basename[:-4] + '_body_normalized.csv'

        loss_filename = dir_name + '/' + basename[:-8] + '_loss.csv'

        # -------------------------------------------------------------------------------
        # load loss value

        with open(loss_filename, 'r') as f:
            reader = csv.reader(f)
            # header = next(reader)  # ヘッダーを読み飛ばしたい時

            for row in reader:
                # arr.append(row[0])
                # print(float(row[0]))
                y = np.append(y, [[float(row[0])]], axis=0)

                # print(row[0])
                break

        # -------------------------------------------------------------------------------
        # load finger motion and pressure

        df = pd.read_csv(files)

        df_finger_motion = df[['finger0', 'finger2', 'finger4', 'finger6', 'finger8', ]] / scale_norm_finger_motion
        df_finger_pressure = df[['finger1']] / scale_norm_finger_pressure
        df_fingers = pd.concat([df_finger_motion, df_finger_pressure], axis=1)

        # df_fingers.to_csv(output_filename1)

        X_fingers = np.append(X_fingers, [df_fingers.values], axis=0)

        # -------------------------------------------------------------------------------
        # load body motion

        df_body_head = (df[['head_x', 'head_y', 'head_z', ]] - center_location_norm_body) / scale_norm_body
        df_body_neck = (df[['neck_x', 'neck_y', 'neck_z', ]] - center_location_norm_body) / scale_norm_body
        df_body_torso = (df[['torso_x', 'torso_y', 'torso_z', ]] - center_location_norm_body) / scale_norm_body
        df_body_waist = (df[['waist_x', 'waist_y', 'waist_z', ]] - center_location_norm_body) / scale_norm_body
        df_body_left_shoulder = (df[['left_shoulder_x', 'left_shoulder_y',
                                     'left_shoulder_z', ]] - center_location_norm_body) / scale_norm_body
        df_body_left_elbow = (df[['left_elbow_x', 'left_elbow_y',
                                  'left_elbow_z', ]] - center_location_norm_body) / scale_norm_body
        df_body_left_hand = (df[['left_hand_x', 'left_hand_y',
                                 'left_hand_z', ]] - center_location_norm_body) / scale_norm_body
        df_body_right_shoulder = (df[['right_shoulder_x', 'right_shoulder_y',
                                      'right_shoulder_z', ]] - center_location_norm_body) / scale_norm_body
        df_body_right_elbow = (df[['right_elbow_x', 'right_elbow_y',
                                   'right_elbow_z', ]] - center_location_norm_body) / scale_norm_body
        df_body_right_hand = (df[['right_hand_x', 'right_hand_y',
                                  'right_hand_z']] - center_location_norm_body) / scale_norm_body

        df_body_normalized = pd.concat(
            [df_body_head, df_body_neck, df_body_torso, df_body_waist, df_body_left_shoulder, df_body_left_elbow,
             df_body_left_hand, df_body_right_shoulder, df_body_right_elbow, df_body_right_hand], axis=1)

        # df_body_normalized.to_csv(output_filename2)
        X_body = np.append(X_body, [df_body_normalized.values], axis=0)

    model.compile(loss='mean_squared_error', optimizer=opt)
    hist_model = model.fit(x=[X_fingers, X_body], y=y, epochs=100, validation_split=0.2, batch_size=32)
    val = hist_model.history['val_loss'][-1]

    trial.set_user_attr('loss', hist_model.history['loss'])
    trial.set_user_attr('val_loss', hist_model.history['val_loss'])
    trial.set_user_attr('loss_final', hist_model.history['loss'][-1])
    trial.set_user_attr('val_loss_final', hist_model.history['val_loss'][-1])

    return val


if __name__ == '__main__':
    study = optuna.create_study()
    study.optimize(objective, n_trials=500)

    print('Number of finished trials : ', len(study.trials))

    print('Best trial:')
    trial = study.best_trial

    print(' Value: ', trial.value)
    print(' Params: ')
    for key, value in trial.params.items():
        print('     {} : {}'.format(key,value))

    print(' User attrs: ')
    for key, value in trial.user_attrs.items():
        print('     {} : {}'.format(key,value))

    path_w1 = 'result_params.txt'
    with open(path_w1, mode='w') as f:
        for key, value in trial.params.items():
            f.write('     {} : {}'.format(key, value))

    path_w2 = 'result_user_attrs.txt'
    with open(path_w2, mode='w') as f:
        for key, value in trial.user_attrs.items():
            f.write('     {} : {}'.format(key, value))

計算結果

1st トライ

  • とりあえず、目的関数の1回の学習計算を100エポックで行い、ハイパーパラメータ評価を500トライアル行うようにしてみた。
  • 大体僕のPCで8時間かければ終わると思ったので、就寝して朝確認してみた。
  • 結果・・・メモリが死ぬほど消費されて、計算が激遅になっていて、全然最後まで計算できてなかった・・・。16GBじゃ小さかったか。もしくはトライアル回数が多すぎたか。
    f:id:surumetic-machine-83:20190114114601p:plain
    朝起きたら、メモリ使用量がフルになって処理速度激落ちしていた・・・。
  • とりあえず途中で切ったが、そのときの一番最後のトライアル時点で、以下のようであった。
    • Current best valueがつまり、最適と判断したハイパーパラメータでの目的関数の値である。  - 手で設計した値で0.0374・・・くらいだったので、微妙によくなった程度。もっと良くなってほしいなぁ・・・。
Current best value is 0.03668692404261002 
with parameters: {
'n_units_body_l2': 289.4605170198315,
'dropout_rate_body_l0': 0.8364339589557341, 
'dropout_rate_finger_l0': 0.8859765071323414,
 'dropout_rate_body_l1': 0.15985137870934507, 
'weight_decay': 2.488304298517825e-05, 
'dropout_rate_finger_l1': 0.341986720233033,
 'dropout_rate_integrated_l0': 0.5575471944470913, 
'n_units_finger_l0': 70.38427639988954, 
'dropout_rate_body_l3': 0.07259930169805667,
 'n_units_body_l3': 53.536974517157255,
 'n_layers_body': 4, 
'dropout_rate_body_l2': 0.2272064995599263,
 'n_layers_integrated': 1, 'n_units_body_l1': 46.97294594609835, 
'n_units_integrated_l0': 34.01545234457688, 
n_units_body_l0': 399.85159118050177, 
'optimizer': 'ADAM',
 'adam_lr': 0.00014800027376498905,
 'n_units_finger_l1': 29.76813380928397,
 'n_layers_finger': 2}.

2ndトライ

  • メモリ食い過ぎたのが、目的関数の試行回数が多すぎて、目的関数評価履歴データが溜まりすぎたのかな?と思い、目的関数の1回の学習計算を1000エポックで行い、ハイパーパラメータ評価を50トライアル行うようにしてみた。
  • また、前回の計算では、最後のほうはほぼNadamかAdamしか選ばれていなかったので、SGDは探索から除外した。
  • 2時間半くらいで終わり、以下の結果が得られた。
  • 最終的な値としては、0.036185・・・になったので、手で設計したときのモデルに比べると0.0015くらいval_lossが改善されている。しかも、圧倒的にパラメータ数が減っている。
Best trial:
 Value:  0.03618520897669861
 Params: 
     n_units_body_l2 : 235.38956630848222
     dropout_rate_integrated_l1 : 0.5737162875000977
     n_layers_integrated : 2
     dropout_rate_body_l2 : 0.9891436475997171
     dropout_rate_body_l1 : 0.1555186733167152
     dropout_rate_finger_l2 : 0.3464849954214746
     n_units_finger_l2 : 49.48920178671224
     dropout_rate_body_l3 : 0.3426272345026113
     n_units_finger_l1 : 103.31434565170868
     n_units_body_l0 : 34.571351359102025
     dropout_rate_finger_l0 : 0.7604449168769942
     optimizer : NADAM
     schedule_decay : 9.964837485839651e-06
     nadam_lr : 1.3501789024608659e-06
     dropout_rate_finger_l1 : 0.3228038832180243
     n_units_body_l3 : 185.55310387909378
     n_units_body_l1 : 14.346523075661459
     n_units_finger_l0 : 25.094171936721253
     n_layers_finger : 3
     n_units_integrated_l0 : 151.11179196621413
     dropout_rate_integrated_l0 : 0.38696485442569295
     n_units_integrated_l1 : 101.67919179045141
     n_layers_body : 4
     dropout_rate_body_l0 : 0.1023105039740452

終結果のモデル図

f:id:surumetic-machine-83:20190114205956p:plain
Optunaでハイパーパラメータ最適化したダーツスキル評価DLモデル

所感

  • Optunaでハイパーパラメータ最適化をトライしてみた。手で設計するよりも、まぁまぁ良いパフォーマンスのモデルができたと思う。
  • ただ、ハイパーパラメータ最適化で劇的に良くならなかったのは、学習データのせいもあると思う。そもそもデータ数が少ないし、(自分でデータ化しておいてなんだが)データの質も高くないしね。今後もっとマシなデータで試してみたいな。
  • クラスごとのデータ量にかなり偏りのあるデータなので、最近話題のFocal Lossを導入してみると、効果がでるかもなと思っている。
    • 識別が簡単な大多数に対する損失の重みを軽くし、識別が困難な少数のクラスに対して損失の重みを大きくする。データに含まれる各クラスのデータ量のばらつきが大きい時に特に効果を発揮するらしい。KITTI Benchmarkの3D Object DetectionのSOTAになっているPointRCNNとかでも使われている。

arxiv.org

今後

  • Focal Lossを試す、LSTMとGRUの導入。もしかしたらCNNもちょっといれる。
    • ぶっちゃけLSTMとか簡単に試すだけならやったことあるけど、どう使うのが効果的なのか考えあぐねている・・・まぁその悩んだ結果もそのうち書ければと。論文読まないとなー。
  • ちゃんと時系列処理した奴に対してOptunaかけて、もう一回記事をポストしたいですね。

MFT2018向けダーツスキル解析(8) - 採点結果例

はじめに

  • 久しぶりに更新です。ずっとサボっていてすみません。
  • 8月にMFT2018に出展してから、本業が非常に忙しく、もうダーツのスキル評価システムの開発は全くやってませんでした。 あーそんなのやってたっけ?って思うくらいになってました。
  • 特に何かに駆られているわけでも、別のコンテストに新たに出すわけでもありませんが、またちょっと気が向いたので、少し手をつけていきたいと思います。

背景

  • 以前、MFT2018に出展しましたが、色んな方に実際にダーツを投げていただきました。その時のデータを元に、本当に「ダーツの投げ方のウマそうな人には、ハイスコアが出ているのか?」を今一度調べてみました。
  • 投げた人のgif動画とセットで点数を載せます。ちなみにあまり意味はないかもしれませんが、プライバシー対策として、顔付近にはやや濃い目の黒いマスクを(雑ですが)かけておきました。

念の為再度言っておくと・・・

  • このシステムは、「ダーツの投げ方」に対して、フォームがどれだけ良いかをdeep learningにて点数化するものであり、そのときダーツの矢がどこに刺さったかは関係しません。
  • このシステムのdeep learningモデルは僕の投げ方しか覚えさせていません。僕がインブルを刺した時のフォームが最適なフォームとして覚えているものです。あくまで僕のフォームが基準です。(ダーツガチ勢の方々ごめんなさい)

得点例(比較的まともに採点できていそうな例)

0-40点の例

f:id:surumetic-machine-83:20190113210332g:plain
0.4点

f:id:surumetic-machine-83:20190113210015g:plain
17.2点

40-60点

f:id:surumetic-machine-83:20190113205343g:plain
29.0点

f:id:surumetic-machine-83:20190113205226g:plain
35.9点

f:id:surumetic-machine-83:20190113205123g:plain
38点

60-80点

f:id:surumetic-machine-83:20190113205545g:plain
61.7点

f:id:surumetic-machine-83:20190113205713g:plain
61.1点

f:id:surumetic-machine-83:20190113205816g:plain
75.2点

80-100点

f:id:surumetic-machine-83:20190113203946g:plain
96.8点

f:id:surumetic-machine-83:20190113203456g:plain
91.2点

f:id:surumetic-machine-83:20190113204934g:plain
89.2点

得点例(絶対ダーツうまそうな人なのに、点数が低い例)

f:id:surumetic-machine-83:20190113210844g:plain
30.0点

f:id:surumetic-machine-83:20190113211020g:plain
2.5点

まとめ

  • 前稿にも書いたが、全体としては割と「それっぽく」フォームの上手下手を判定できていると思う。(いい加減なDLモデル使ったにしては、よく出来ている・・・)
  • やはりセンサグローブで指圧や指の曲げ具合も見ているので、グリップが僕と違う人は点数が低かったと思います。
  • そうはいっても、やっぱりフォームから見てどう見ても点数が低いケースがあるので、どうかなぁと思います。改善したいですね。
  • これから更に少しずつ頑張ります。

今後の投稿予定

  • LSTM, GRUを用いた時系列考慮のネットワーク利用結果
  • Optunaを用いたモデルハイパーパラメータの最適化

Maker Faire Tokyo 2018 出展レポート

f:id:surumetic-machine-83:20180806010334j:plain

背景

  • 色々ありましたが、Maker Faire Tokyo 2018 (MFT2018)に無事「ダーツ自由形評価システム」を出展できました!
  • MFT2018は8/4,5でしたが、本ブログ見て頂くと分かる通り、8/3の夜まで上手下手判定が全くできておらず、絶望感あふれるブログだったのですが、なんとかそれっぽい展示ができてよかったです笑

前回ブログ記事からの進捗

  • ダーツの矢がダーツボードにささった瞬間から、ダーツの上手下手判定(というよりは、私が投げてインブルに入った時のフォームにどれだけ近いかを判定するAI判定・・・)の処理を走らせ、どれだけうまいかを100点満点で表示する機能を作りました!(8/4の朝)

  • 実際に綺麗になげたときのスコア判定の様子はこちら↓

  • ジャンプして投げると、フォームが狂ってると判断されてスコアが悪くなります↓

当日の様子とか

  • ブースの様子 f:id:surumetic-machine-83:20180805235640j:plain

  • この看板でかなり多くのひとの注目を集められました。書いてくれた協力者に感謝! f:id:surumetic-machine-83:20180805234640j:plain

  • 本当は、いろんなひとに投げてもらった時の写真をここに載せたいけど、肖像権とかプライバシーとかあるので、やめておきます笑

トラブル - 会場のライトが強すぎたので・・・

-8/4初日、ブースを設営してRGBDカメラ(Xtion)で体の関節を計測しようとしたところ、うまく計測できず。なんでか?といろいろ考えた結果、会場のライトが強すぎて、カメラ画像が一部白飛びしていることに気づきました。

f:id:surumetic-machine-83:20180805235255j:plain

  • そこで急遽、当日持っていたサングラスをXtionにかけさせました。光量がカットされて、とてもよく関節を認識できるようになりました。
    • 余談ですが、Xtionよりもサングラスのほうが高価です。

f:id:surumetic-machine-83:20180805235159j:plain

反省

  • 2日目(8/5)から本ブログへのリンクのQRコードをブースに貼ったのですが、初日(8/4)全く宣伝的な活動をしていませんでした(QRコード貼るだけなのも十分か微妙ですが)。2日目のブログの閲覧数見て、「こんなに閲覧数上がることがあるんだなぁ」と思いました。全体的に宣伝がヘタすぎました笑
  • 今回のDLモデルで、「固定長の時系列から、1つの値を求める単純な問題にしているから、LSTMとかRNNみたいなレベルの構造は要らないと思うんです」とブースに来たひとに説明して押し切ってしまった感じがありますが、あくまで僕がよく分かってないだけの可能性があるので、しっかりと決めつけず議論して、生産的な場にすべきだったなぁと反省しました。あくまで僕はデータ解析とか全然素人なので、もっと色んな意見を仕入れる努力が必要でした・・・。

今後

  • 今後も今回作ったシステムを更に向上させていくのか?というと、ちょっとどうしようかなと思うところです。
  • 例えば「グローブがダーツの矢を持ちにくくしてるし、手の甲のセンサが重いから投げにくいので何とかしてほしい」という意見もありましたが、そういうハードウェア的な対応は自分は苦手なので、「よし、対応しよう!」という気持ちがあまり湧いていません。こういうときこそひとを頼るべきなのか?
  • データは色々取れたので、勉強がてら色んなデータ解析をしてみようと思いますが、システムとしてのAI判定精度を高めるためにはやはりより多くのデータが必要です。しかしながら、特にプロダーツプレイヤーの知り合いがいるわけでもないし、マネタイズしてうまく関係者を儲けさせるとかは苦手分野なので、ちょっと自信がありません。
  • もしかしたら、あと数カ月後にはこのシステムを全てプログラムとか構成とか全てネットでオープンにして放り投げ、別のプロジェクトに取り組むかもしれません。

振り返って

  • 今回の出展は、とりあえず初めて一人で何か作って出展すること!でした。とりあえず諦めずに出展までたどり着いたので、自分自身には100億万点くらいあげたいですね(激甘)。
  • とはいえ、一人でシステムは作ったものの、展示するときのブース用意(壁とか看板とか)や説明員の数とかいう意味で、全然一人で回せず、友達に助けてもらいました。やっぱ何でも一人でやろうとするのは良くないかもしれません。
  • 展示を見に来てくださった方の中に、「プロなのでは?!」と思われるハイレベルな感じのダーツプレイヤー的な方が何人かいらっしゃったのですが、AIに教えた僕と投げ方&矢の握り方が大きく違ったので、あまりスコアが高くなくて申し訳なかったです。
  • 「展示して終わりかな」、と思っていましたが、別の展示会やコンテストへの出展のお誘いや、転職のお誘い(笑)も頂けましたし、色んなひととディスカッションできて、自分にとってすごく得るものの多いイベントでした。

まぁとにかく、出展してよかったです!