頭と尻尾はくれてやる!

iOSアプリなどの開発日記です


機械学習で人体頭部の姿勢を得る(3)

機械学習で人体頭部の姿勢を得る(2)
↑この続き。
もう一つうまくいかないのはもしかして教師データが甘かったんじゃなかろうか?と。
例えば正面をゼロとして-15°から+15°までの画像をランダムに作成してデータセットを作成した場合、+45°の画像を入力とした時に正解を推論できるのか?そりゃ無理なんじゃ、、、?

そんなわけで従来より大きく動くようにしてデータセットを作成してみた。
データ数で30,000。従来の100,000は”穏やかな”データに加えて合計130,000個。
これで最初からやった方がいいのかよくわからないので、従来の続きでぶん回してみた。

TensorFlowの途中経過

↑そりゃ、そうなるわなってくらい損失関数の値が跳ね上がった。ここから学習していくのか?って思ってたんだが、なんとか学習は進んでるみたい。

ところが、テキトウな画像に対して推論すると相変わらずよろしくない結果に。こりゃダメだ、、、
スポンサーサイト






機械学習で人体頭部の姿勢を得る(2)

機械学習で人体頭部の姿勢を得る
↑の続き。頭髪なしの男性モデルだけではさすがにダメなのかということで、いろいろと追加してみた。

データセットの一部

↑左上の頭髪なし男性が元々のデータ(データ数40,000)。
下の6パターンが今回追加したもの。
男性に3パターンで髪の毛を付け、女性モデルも追加。各10,000ずつの60,000で合計100,000のデータ数になった。ちなみに女性モデルの右二つは髪のオブジェクトの左右をひっくり返しただけの違い。

データを作る所や使う所はデータが後から増えても簡単に対応できるようにしておかないとあかんね(実は最初の頃はそこまで頭が回ってなかったんだけど)。

15.5エポックぶん回して、、、前と同じ画像で評価してみたのがこちらの動画。



少し改善した気はする。前は岡崎体育さんが正反対向いてたし。
でもまだおかしいのもあるなあ。トレーニングをさらにすれば改善するのかデータの種類を増やせばいいのか、NNの構成を再考すべきか、、、


機械学習で人体頭部の姿勢を得る

3Dのボーン付き人体モデルをSceneKitで使う
↑iOS上でこんな感じでリアルな人体の頭部を動かしてその画像(白黒、64x64)と姿勢データ(neck boneの座標x,y,z、クォータニオンx,z,w、head boneのクォータニオンx,y,z,w)の10個の値(どれも-1から1)を持つデータセットを作成。データ数は40,000個。

頭部のbone構成

↑こんな感じで接続してる。neck boneは軸周りの回転はなしとしてたのでy成分はゼロで実質x,z,wの3個。

損失関数は単に正解との差の絶対値の総和からの平均、という誰でも最初に思い付きそうなモノ。


PythonでTensorFlow使って学習を進めた。
まだ試行錯誤の途中だけど、現時点でのNNの構成は
畳み込み→プーリング→lrn→畳み込み→畳み込み→lrn→プーリング→全結合→ドロップアウト→全結合
てな感じ。Cifar-10のサンプルコードをめっちゃ参考にしてる。

その結果がこんな感じ↓


画面下の小さい画像から白黒の画像データを得て、予測、その結果を人体モデルに反映(画面上部)。
最初の10枚は(訓練用ではなく)テスト用画像のもの。そこそこ姿勢は得られている感じ。
と言っても訓練用もテスト用もたった一人の人体モデルからデータセットを作ってるので、他の人の画像を入力したらどうよ?ってのが後半の10枚。

なんとなくいけてそうなのもあるし、南野陽子さんや岡崎体育さんみたいに反対向いてるのもある。
岡崎体育どっち向いてるねん?!とネタにしたいとこだけど元データがハゲのおじさんデータだけなので仕方ないんだろう。いや、実は頭髪や性別など関係なく目、鼻、口の位置から予測してたらいけるのかも?!なんて期待してたんだけどね。
そんなわけで今後は元データ人体の種類を増やしてみるかなあ。

とりあえずMNISTやCifar-10などのように分類するのと違って出力で複数の値を得る、ということができそうということがわかった、ってとこがここまでの成果か。


TensorFlowのlrn層ってMPSではどう記述するの?

TensorFlowの学習結果をiOSのMPSで使う
↑この続きで畳み込みのある場合のMNISTをやってみたら(珍しく)一撃でうまく行ったのはいいんだけど、その次に別なNNでやろうとしてはまったんよ。

TensorFlowで tf.nn.lrn (local response normalization)ってのがあるけど、これをiOS側のMetal Performance Shadersではどう表現するのかよくわからん。色々調べたり試してみたんだけど、MPSのMPSCNNCrossChannelNormalizationでいいのかなと。
ただ、双方の定義をよくみるとどこからどこの層を対象にするねんってところで双方のパラーメータをどうやっても完全に一致はしないよね?

norm1 = tf.nn.lrn(h_pool1, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75, name='norm1')
↑こんなTensorFlowのlrn層(値はCifar-10であったやつかな)をMPSで使おうとした時、
kernelSizeは9(これはプロパティじゃなくイニシャライザで設定)
norm.alpha = 0.001;
norm.beta = 0.75;
norm.delta = 1.0;
でいいのかなという気がするんだが、、、?定義からするとalphaの/9は不要と思うんだが、実はこの/9の有無で出力の値に変化がなかったのでよくわからない。まあいい、俺はそうするよ、lrn層後の値は双方似たような値になるし。

ただ、完全に一致するというわけじゃないのよ(入力データは完全に一致するけど)。

そもそもこのlrn層の前の畳み込み層の出力からしてTensorFlowとMPSでは値が微妙に違う。

0.15247595 0.13704236 0.24492025
0.152466 0.136963 0.244873
上がTensorFlow、下がMPSでの畳み込み層後の値。

TensorFlowでは32ビット浮動小数点数を使うけど、MPSでは16ビットだからそのあたりで異なってくるのかなあと思ってる。


↓参考)双方のリファレンス
tf.nn.local_response_normalization  |  TensorFlow
MPSCNNCrossChannelNormalization - MetalPerformanceShaders | Apple Developer Documentation


TensorFlowの学習結果をiOSのMPSで使う

PythonでTensorFlow使って簡単な方(つまり畳み込みじゃない方ね)のMNISTをやって、計算結果の係数(重み、バイアス)を出力して、iOSのMetal Performance Shadersで使い、手書き数字認識させるのにえらくハマったのでそのあたりのメモ。ちなみにiOSで使うのはObjective-Cです、はい。Swiftだったらサンプルコード(MPSCNNHelloWorld)もあるのでだいぶラクだったとは思いますけどね。

iOSでもTensorFlowが使えるんだけど、ここはやっぱりMPSの方がアプリの容量も小さくなるらしいのでMPSで。

iOSのMPSCNNに渡すモデルパラメータのフォーマット / TensorFlowからの書き出し - Qiita
↑基本こちらを参考にさせていただきました。

まず、Pythonでの学習後に係数を出力するところ。
{
	with open(filePath_iOS+'weights.dat', 'wb') as f:
            W_trans = tf.transpose(W, perm=[1,0])
            f.write(sess.run(W_trans).tobytes())
	with open(filePath_iOS+'bias.dat', 'wb') as f:
            f.write(sess.run(b).tobytes())
}
参考のページと違って open( , ‘w’) → open( , ‘wb’) にしないとダメだった。
あと、重みは順番を変更。

次にiOS側。
わかりにくいのがGPUでの結果が16bitの浮動小数点数で出てくるけどCPUで使う場合にはfloatは32bitだというところ。
Swift で 半精度浮動小数点数 (16 bit Float) を扱う - Qiita
↑こちらなどを参考に16bit→32bitとして読む、というのをできるようにならないとデバッグが進みませんでした。

-(float)getFloatFromUInt16:(UInt16)value
{
    UInt16 *input = calloc(1, sizeof(UInt16));
    input[0] = value;
    float *output = calloc(1, sizeof(float));
    output[0] = 0.0;
    
    vImage_Buffer sourceBuffer;
    sourceBuffer.data = input;
    sourceBuffer.height = 1;
    sourceBuffer.width = 1;
    sourceBuffer.rowBytes = sizeof(UInt16);
    
    vImage_Buffer destinationBuffer;
    destinationBuffer.data = output;
    destinationBuffer.height = 1;
    destinationBuffer.width = 1;
    destinationBuffer.rowBytes = sizeof(float);

    vImageConvert_Planar16FtoPlanarF(&sourceBuffer, &destinationBuffer, 0);
    
    float floatValue = output[0];
    
    free(input);
    free(output);
    
    return floatValue;
}
↑こういうのを作っといて入出力画像のMTLTextureの中身なんぞを確認してました。

無駄にハマったのが入力画像をセットする
replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage:
のwithBytes:のところに context と書いてて正しく動かず(実行可能だけど正解が出ない)悩んでた。
正解はCGBitmapContextGetData (context)

サンプルコード(Swift)で
context!.data!
となってるのに、contextとするだけで必要なデータが取れると思ってたのよ。


ともかくこれで、Pythonで学習させ係数を出力、iOSのMPSで使う、って流れの第一段階はクリアできたかな。
次は畳み込みの場合でできるようにならねば。


  TopPage  



Copyright ©頭と尻尾はくれてやる!. Powered by FC2 Blog. Template by eriraha.