gggggraziegrazie

graizegrazieさんのやったこと、学んだことを記録する雑記帳です

決定木(Decision Tree)

決定木とは、複数のルールを使って組み合わせることで、データを複数のサブセットに分割する手法のことです。分割したサブセットの内容により、分類木や回帰木と呼ばれます。分類木の分割対象はクラスであり、回帰木の対象は出力値です。つまり決定木の作成後に新しいデータを得ると、分類木を使えばそのデータが所属するクラスを、回帰木を使えば対応する出力値を求めることができます。下記の図が理解の参考になるかと思います。

f:id:graziegrazie:20181223185808p:plain
Fig. 分類木と回帰木の例
左:分類木の例(暑いと感じる温度・湿度のペアを入力した際に、暑い・暑くないといくクラスを出力する木)、右:回帰木の例(温度・湿度を入力した際に、何Lの水を飲むかを出力する木)(出典[1])

f:id:graziegrazie:20181223212445p:plain
Fig. 決定木の入出力例と回帰木の出力例[4]

なお決定木はアルゴリズムの名前ではなく、モデルの名前です。モデルの構造が木構造のため、その名がついています。

C++における集合演算のための関数たち

席集合や和集合などを作る関数がないか調べたところ、STLとBoostでそれらの関数を見つけたのでメモします。なお、STLとBoostでそれぞれ関数名は一緒です。挙動や入出力は異なりますので、詳細は下記の参考文献をご覧ください。

和集合: set_union
積集合: set_intersection
差集合: set_difference

具体例については、このページをご覧頂いくのがよいかと思います。

外れ値・異常値の検出手法

外れ値と異常値

外れ値とは、原因が不明だけれども真値から大きく異なる値のことです。一方異常値とは、同じく真値から大きく値が異なるけれども、何故値が真値と異なるのか説明がつく(測定ミス、記入ミスなど)値を指します[4][5]。つまり、異常値 \subset 外れ値となります。

検定(仮説検定)

現在注目している値が、外れ値/異常値が真値ではないという仮説を統計学的に検証するための手法のことです。検定においては、棄却したい仮説を帰無仮説と言い、採択したい仮説を対立仮説と言います。対立仮説は、帰無仮説の対偶と言うことができます。

下記に今回調べた検定手法について列挙していきます。[6][7]

スミルノフ・グラブス検定[1][8]

正規分布に従うデータについて、外れ値を検出する手法の1つである。そのためデータが正規分布に従っていないと、真値すらも外れ値と見なしてしまうので注意が必要である。

帰無仮説:全データは同じ母集団から発生している
対立仮説:データ中の最大・最小値は外れ値である

ここで標本平均を\bar{X}、普遍分散を Uとしたとき、最大または最小の測定値X_iについて、
T_i \  = \  \frac{|X_i \  - \  \bar{X}|}{\sqrt{U}}
を求める。この値がt分布から求めた有意点tを下回るとき、帰無仮説は棄却され、測定値X_iは外れ値でないと判断される。

なお普遍分散とは、標本分散の期待値が母分散(母集団の分散[9])に一致するように \frac{n}{n-1}を掛けた値のことである。

なおt分布の自由度とは、標本サイズ - 1の値だそうです。つまり標本数が500の場合、自由度は499となります。[10]

Hampel Identifier

四分位範囲

DBSCAN

計算速度が遅いので、リアルタイム性が求められるアプリケーションに適さないそうです[2]

Isolation Forests [3]

Median Absolute Deviation

Numpyでの二次元排列の操作方法

順次記載内容を追加してきたいと思います。

二次元配列の作り方
arr = np.empty((0, 2) , float) # 0行2列の二次元配列オブジェクトを生成する
arr = np.append(arr, np.array([[x, y]]), axis=0) # np.appendは演算結果を返すのみで、引数を更新しない点に注意
                                                                        # 二次元配列に要素を追加するためには、追加する要素も二次元配列の形式で記述する必要がある
                                                                        # 行を増やす場合はaxis=0, 列を増やす場合はaxis=1

なお上記はnumpyを使った二次元配列の作り方を述べましたが、もちろんビルトインクラスのリストでも二次元配列を作ることができます。[1]曰く、リストクラスの方が高速に配列を増やすことができます。その一方で、numpyを使った方が、[2]に記載の様にスライスの記法がわかりやすいというメリットがあります。状況に応じて使い分けることをおすすめします。

PCL中のGeneralized ICPを調査してわかったことメモ

PCL中でGeneralized ICP(以降GICP)を調査してわかったことを書き連ねます。適宜更新するため、読みづらいところがあるかもしれませんがご容赦ください。

GICPを実行するには、ICPと同様にalignを行えばよい。Levenberg–Marquardt法を使いたいんだけどと思うかもしれませんが、この関数はalignの中で呼ばれています。alignの中で呼ばれているrigid_transformation_estimation_は変数で、gicp.hにてestimateRigidTransformationBFGSがバインドされています。

ただしLevenberg–Marquardt法で使うパラメータは、gicp.hppのestimateRigidTransformationBFGS内にハードコーディングされているため変更は出来ません。つまり、GICPの1ループの中でどれだけPointCloudを動かすのかについて、パラメータを介して関与できません。関与できるのは収束判定の部分です。

コマンドライン中で指定したパラメータ名をノード中で取得する方法(C++)

結論から言うと、2種類の方法があります。

  1. ros::names::remap : 返り値としてremapされたパラメータ名が取得できます。
  2. resolveName : 返り値としてremapされたパラメータ名が取得できます。第二引数でremapが成功したかをboolで取得できます。
ros::names::remap

remapされた名前は返り値として返されます。また返された名前は、絶対パスで返されます。つまり、

std::string remapped_name = ros::names::remap("hoge")
std::cout << remapped_name << std::endl;

というコードがノード中に書いてあり、さらに

rosrun node node hoge:=fuga

を実行すると、

/fuga

が表示されます。これはros::names::remapがノードハンドルのメンバ関数ではないためと考えれます。

resolveName

remapされた値は、ノードハンドルのNameSpaceによって変化します。例えば
コード中に

ros::Nodehandle n;
bool resolve_result;
std::string remap = n.resolveName("hoge", resolve_result);
std::cout << remap << std::endl;

と書いてあり、

rosrun node node hoge:=fuga

を実行すると、

/fuga

が表示されます。また

ros::init(argc, argv, "node_name");
ros::Nodehandle n(~);
bool resolve_result;
std::string remap = n.resolveName("hoge", resolve_result);
std::cout << remap << std::endl;

と記載がある状態で

rosrun node node hoge:=fuga

を実行すると、

/node_name/fuga

が表示されます。こちらはノードハンドルのメンバ関数のため、ノードハンドルの持つNameSpaceの影響を受けることがわかります。

ご参考になりましたら幸いです。

ROSにおけるシャットダウン処理の書き方

ROSのノードを動かした時、Ctrl-Cでそのノードを落としたいのに、落ちてくれない場合ありますよね。また落ちたとしても、下記の様なexceptionが発生したりするなどして、キレイに落ちてくれなかったりします。

terminate called after throwing an instance of 'boost::exception_detail::clone_impl >'
what(): boost: mutex lock failed in pthread_mutex_lock: Invalid argument
Aborted (core dumped)

これは[1]にもありますが、callbackやtimerが動いているのが問題です。そのため[1]に記載の様にシグナルハンドラを設定し、そのコールバック中でtimer[3]やsubscriber[2]を止め、さらにノードのshutdownを行なう必要があります(下記参照)。

signal(SIGINT, signal_handler);
void signal_hander(int signum)
{
/* 下記がtimerやsubscriber, nodeの停止用関数。実行するタイミングはプログラムに依存するので、必ずしもハンドラで実行すればよい訳ではない */
  timer.stop();        // timerの停止。この関数はtimerの中で呼んだ方がよいかもしれない。
  sub.shutdown(); // subscriberの停止
  ros::shutdown(); // ノードの停止。これを呼ぶとspinから抜ける。
}

上記終了処理に伴い、main関数中でspin関数を呼ぶ部分は下記の様に書く必要があります。spinOnceでなくspinを使ってしまうと、シグナルハンドラは呼ばれるものの、spin関数から抜けられないためにwhile文から抜けられません。終了処理を書きたい場合は、spinOnceを使うようにしましょう。そんなことはありません、ちゃんと抜けます。下記のcase1, 2のどちらでも構いません。

/* case 1 */
ros::spin();
/* case 2 */
ros::Rate rate(1);
while( false == ros::isShuttingDown() )
{
    ros::spinOnce();
    rate.sleep();
}