目次
こんにちは.高山です.
以前の記事で,Numpyの行列計算を用いて線形補間を行う方法を紹介しました.
今回は,同様の処理をTensorflowを用いて実装する方法を紹介します.
今回解説するスクリプトはGitHub上に公開しています.
本記事の実装方法は,Brent M. Spell氏のブログに記載の方法を参考にしています.
この度,本記事向けに拡張したコードを記載してよいかお伺いしたところ,快く許可してくださいました.
この場を借りて感謝申し上げます.
更新履歴 (大きな変更のみ記載しています)
- 2024/09/17: タグとサマリを更新しました
- 2023/10/30: 処理時間の計測方法を更新しました
1. モジュールのロード
まず最初に,下記のコードでモジュールをロードします.
1 2 3 4 5 6 7 8 9 10 |
|
【コード解説】
- 標準モジュール
- gc: ガベージコレクション用ライブラリ
処理時間計測クラスの内部処理で用います.
- sys: Pythonシステムを扱うライブラリ
今回はバージョン情報を取得するために使用しています.
- time: 時刻データを取り扱うためのライブラリ
今回は処理時間を計測するために使用しています.
- functools: 関数オブジェクトを操作するためのライブラリ
処理時間計測クラスに渡す関数オブジェクト作成に使用しています.
- 画像処理・機械学習向けモジュール
- numpy: 行列演算ライブラリ
今回はデータのロードに用います.
- Tensorflow: ニューラルネットワークライブラリ
今回は行列演算機能を用います.
記事執筆時点のPythonと主要モジュールのバージョンは下記のとおりです.
1 2 3 |
|
Python:3.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.0]
Numpy:1.23.5
Tensorflow:2.14.0
2. 入力データのロード
次に,下記の処理で補間前と補間後の追跡点データをダウンロードします.
補間後の追跡点データは以前の記事で紹介した処理で生成したデータです.
このデータは今回の処理で生成したデータとの比較に用います.
!wget https://github.com/takayama-rado/trado_samples/raw/main/test_data/finger_far0_non_static.npy
!wget https://github.com/takayama-rado/trado_samples/raw/main/test_data/finger_far0_non_static_interp.npy
ls
コマンドでデータがダウンロードされているか確認します.
!ls
finger_far0_non_static_interp.npy finger_far0_non_static.npy sample_data
3. 実験用処理の実装
実験に先立って,処理時間計測用の関数と実験用定数を定義します.
次のコードは処理時間の値から,適切なSI接頭辞を設定し,文字列にして返します.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
- 引数
- val: 処理時間を示す値 (秒)
- 2-6行目: 変数の初期化
- 7-11行目: 接頭辞を選択して,表示値を調整
- 12行目: 表示文字列を作成
次のコードは,処理時間を格納した配列から統計量を求めて表示します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
- 引数:
- intervals: 処理時間のNumpy配列
- top_k: intervalsから,処理時間が短いサンプルをk個取り出して統計量を算出
- 2-3行目: Top K個のサンプル抽出
- 4-7行目: 統計量算出
- 9-16行目: 表示文字列を作成して表示
次のクラスは,入力した関数を複数回呼び出して,処理時間の統計量を表示します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
- 引数:
- trials: 入力関数の実行回数
- top_k: 値が定義されている場合,全体の処理時間配列から処理時間が短いサンプルをk個取り出して統計量を算出
- 9-10行目: 計測中はガベージコレクションをしないように設定
- 11-20行目: 処理時間計測処理
- 21-22行目: ガベージコレクションの設定を元に戻す
最後に,次のコードで実験用の定数を定義して処理時間計測クラスをインスタンス化しています.
1 2 3 |
|
実際の所,Colabの様な複雑な処理環境で他のプロセスの影響を排除して純粋な処理時間を計測するのはかなり難しいです.
そこでここでは,処理の繰り返し数を \(100\) とし全体の統計量に加えて,処理時間の短い代表値 \(10\) 試行からも統計量を表示するようにしています.
4. 線形補間の実装・実行
ここから先は補間処理の実装・実行を行います.
Tensorflowは,プログラムの実行時に処理をコンパイル (計算グラフの作成) して実行します.
現在のTensorflowには,下記に示すとおり2種類の計算グラフ作成方法があります.
- Eager execution mode (Define-by-Run): データを入力した際に処理のコンパイルと実行を同時に行う.
Pythonの実行環境と親和性が高くインタラクティブな実行環境で利用しやすくなっています. - Graph mode (Define-and-Run): (C言語のように) 処理のコンパイルと実行が明確に別れて行われる.
処理の最適化性能が高いため高速に動作します.
今回は,補間処理をそれぞれの動作モードで動かして処理時間を比較してみたいと思います.
4.1 Eager execution modeを用いた場合
本項ではまず,Eager execution mode を用いた場合の実装を紹介します.
と言っても特別なところはあまりありません(^^;).
現在のTensorflowは Eager execution mode が標準の動作モードになっているため,処理をそのまま書いていけばこちらの動作モードで実行されます.
補間処理の実装
次のコードでは,こちらの記事の第2節で説明した行列計算を用いた線形補間処理を実装しています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
【コード解説】
- 引数
- track: `[T, J, C]` 形状の追跡点配列.欠損値はゼロ埋めされている必要があります.
また,この関数は部位毎に追跡点配列が入力される (全追跡点,特徴量で欠損フレームが共通) ことを想定しています.
- T: 動画フレームインデクス
- J: 追跡点インデクス
- C: 特徴量インデクス.今回は $(x, y, z, c)$ の4次元特徴量を用いています.
- 2-3行目: 内部処理で用いるため,`track` の形状情報を取得
- 4行目: 第0番目の追跡点の信頼度に基づいて,追跡成功フレームを示す `mask` を生成.
この関数は全追跡点,特徴量で欠損フレームが共通であることを前提としています.
- 5-7行目: 5行目で追跡成功フレーム数を取得し,線形補間が必要かを判定.
Numpyと違ってTensorflowでは暗黙的な型変換は行われないため,`tf.cast()` で明示的に型変換を行っています.
7行目で即座に関数を出ずに `y = track` としている点に注意してください.
Graph mode では `if` 文は `tf.cond()` 関数に変換されます.
そのため,7行目で `return track` とした場合は `if` ブロックと `else` ブロックで同種の値を返していない,としてエラーになります.
今回は動作モード毎にコードを変えたくないので,Graph mode で動作可能な実装方法をしています.
- 9-13行目: 追跡成功フレームのデータ点を取得
- 14行目: 補間箇所を含む,全フレームのインデクス配列を生成
- 15-19行目: 計算処理のために型を `ys` に合わせて変換
- 22-23行目: `xs` と `ys` の先頭と末尾にダミー値を追加
- 26-27行目: 直線の傾き行列を算出し,先頭にダミー値を追加
- 30行目: 直線の切片行列を算出
- 36-38行目: ルックアップテーブルを作成して,全フレームのインデクスに対する直線パラメータを取得
- 41-43行目: 線形補間を実行し,型と形状を入力に合わせて返す
全部位の補間処理
全部位の追跡処理は次の関数で実装してます.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
【コード解説】
- 引数:
- trackdata: `[T, J, C]` 形状の追跡点配列.欠損値はゼロ埋めされている必要があります.
こちらの追跡点には全部位のデータが含まれていることを想定しています.
- 2行目: 内部処理のために,追跡点数を取得
- 3行目: `trackdata` を `numpy.ndarray` から `tensorflow.Tensor` に変換
- 4-7行目: 追跡点を部位毎に分割.Tensorflowでは配列の一部分を取得するのに `tf.gather()` 関数を用います.
- 9-12行目: 部位毎に線形補間を実行
- 13行目: 部位毎の追跡点を結合して返す
補間処理の実行
線形補間に必要な処理が実装できましたので,上でダウンロードしたデータを用いて処理を実行します.
次のコードでは,追跡点をロードして,関数の仕様に合わせて形状を変更しています.
追跡点データは[P, T, J, C]
形状 (P
は人物インデクス) の配列ですが,人物は1名だけですのでP
軸は除去しています.
1 2 3 4 5 |
|
次のコードは先程実装したpartsbased_interp_tf_eager
にtrackdata
を入力して,補間後の追跡点newtrack
を得ています.
1回目の処理呼び出しでは,処理のコンパイルが行われるため個別に時間を計測し,参照用の補間後追跡点との誤差を計測しています.
その後,処理時間計測クラスを用いて平均処理時間を表示しています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
print
処理の結果を示します.
1回目の処理時間は121.0ミリ秒程度で,誤差はほぼありませんでした.
2回目以降の処理では,(他のプロセスの影響が少ない場合は) 11.7ミリ秒程度で処理が完了しています.
Time of first call.
Overall summary: Max 120.998ms, Min 120.998ms, Mean +/- Std 120.998ms +/- 0s
Sum of error:-6.935119145623503e-12
Time after second call.
Overall summary: Max 61.8187ms, Min 11.2679ms, Mean +/- Std 20.8628ms +/- 8.64964ms
Top 10 summary: Max 12.2807ms, Min 11.2679ms, Mean +/- Std 11.7345ms +/- 329.671µs
入力データ形状が変わった場合の挙動
Tensorflowで Graph mode を用いた場合の典型的な問題として,処理の再トレーシングが挙げられます.
これは,入力データの型や形状が変わった場合に処理のコンパイルが再度行われてしまう問題を指します.
Eager execution mode ではこの問題は起きませんが,Graph mode との比較のために下記のコードで挙動を確認します.
処理の再トレーシングについての詳細は,公式ドキュメントをご参照ください.
次のコードは先程の線形補間の実行処理とほぼ同じですが,入力データの時間長が1フレーム分短くなっています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
print
処理の結果を示します.
1回目と2回目以降の処理時間は大きくは変わらず,Eager execution mode では処理の再トレーシングは起きないことが分かります.
Time of first call.
Overall summary: Max 30.9785ms, Min 30.9785ms, Mean +/- Std 30.9785ms +/- 0s
Sum of error:-6.935119145623503e-12
Time after second call.
Overall summary: Max 75.0154ms, Min 15.6051ms, Mean +/- Std 23.0584ms +/- 10.6375ms
Top 10 summary: Max 17.3485ms, Min 15.6051ms, Mean +/- Std 16.8033ms +/- 521.555µs
4.2 Graph mode (tf.function) を"型指定無しで"用いた場合
補間処理の実装
ここから先は,Tensorflowの Graph mode を用いた場合の挙動について見ていきます.
Tensorflowでは @tf.function
デコレータで修飾した関数は Graph mode で動作します.
今回は Graph mode で動作するように線形補間処理を実装しているので,コードの変更はほとんどありません.
次のコードでは,先程紹介した線形補間処理関数を Graph mode で動作するようにしています.
中身のコードは完全に同じなのでここでは説明を省かさせていただきます.
1 2 3 4 |
|
補間処理の実行
次のコードで補間処理を実行しています.
先程と同じく,1回目の処理呼び出しでは,処理のコンパイルが行われるため個別に時間を計測し,2回目以降に処理時間計測クラスを用いて平均時間を表示しています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
print
処理の結果を示します.
1回目の処理では処理のコンパイルの影響で1.6秒程度かかっていますが,2回目以降の処理では9.2ミリ秒程度で処理が完了しています.
Time of first call.
Overall summary: Max 1.5834s, Min 1.5834s, Mean +/- Std 1.5834s +/- 0s
Sum of error:-6.935119145623503e-12
Time after second call.
Overall summary: Max 70.1215ms, Min 8.67015ms, Mean +/- Std 21.3508ms +/- 10.4774ms
Top 10 summary: Max 9.71076ms, Min 8.67015ms, Mean +/- Std 9.19527ms +/- 332.419µs
入力データ形状が変わった場合の挙動
次に,先程と同じく入力データの形状が変わった場合の挙動を示します.
次のコードは先程の線形補間の実行処理とほぼ同じですが,入力データの時間長が1フレーム分短くなっています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
print
処理の結果を示します.
Eager execution mode の時とは異なり,1回目の処理で1.7秒程度かかり,2回目以降の処理では8.0ミリ秒程度で処理が完了しています.
ここから,Graph mode で入力データの形状が変わった場合は,再トレーシングの影響で実行時間が長くなってしまうことが分かります.
Time of first call.
Overall summary: Max 1.73487s, Min 1.73487s, Mean +/- Std 1.73487s +/- 0s
Sum of error:-6.935119145623503e-12
Time after second call.
Overall summary: Max 36.0208ms, Min 7.17009ms, Mean +/- Std 14.598ms +/- 5.84049ms
Top 10 summary: Max 8.39623ms, Min 7.17009ms, Mean +/- Std 7.99962ms +/- 385.734µs
4.3 Graph mode (tf.function) を"型指定有りで"用いた場合
補間処理の実装
最後に,Graph mode 利用時に処理の再トレーシングを抑制する方法を紹介します.
再トレーシングを抑制するには,@tf.function
デコレータの引数として型情報を明示的に与えてあげればよいです.
次のコードでは,@tf.function
デコレータの input_signature
引数に対して, tf.TensorSpec()
を与えています.
tf.TensorSpec()
は入力のメタ情報を記述するためのクラスで,ここでは入力配列の形状と型を記述しています.
shape=[None, None, 4]
の None
は入力毎にサイズが変化する次元を示しています.
今回は時間軸と追跡点軸に対して None
を指定しています.
1 2 3 4 |
|
補間処理の実行
次のコードで補間処理を実行しています.
先程と同じく,1回目の処理呼び出しでは,処理のコンパイルが行われるため個別に時間を計測し,2回目以降に処理時間計測クラスを用いて平均時間を表示しています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
print
処理の結果を示します.
1回目の処理では処理のコンパイルの影響で328.6ミリ秒程度かかっていますが,2回目以降の処理では8.6ミリ秒程度で処理が完了していることが分かります.
Time of first call.
Overall summary: Max 328.611ms, Min 328.611ms, Mean +/- Std 328.611ms +/- 0s
Sum of error:-6.935119145623503e-12
Time after second call.
Overall summary: Max 33.595ms, Min 8.34474ms, Mean +/- Std 10.9117ms +/- 3.95531ms
Top 10 summary: Max 8.8418ms, Min 8.34474ms, Mean +/- Std 8.63372ms +/- 153.86µs
入力データ形状が変わった場合の挙動
次に,先程と同じく入力データの形状が変わった場合の挙動を示します.
次のコードは先程の線形補間の実行処理とほぼ同じですが,入力データの時間長が1フレーム分短くなっています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
print
処理の結果を示します.
前項の結果と異なり,1回目の処理では19.2ミリ秒程度,2回目以降の処理では5.9ミリ秒程度で処理が完了しています.
ここから,Graph mode で実行する際も入力データの型情報を明記することで再トレーシングを抑制することができることが分かります.
Time of first call.
Overall summary: Max 19.1748ms, Min 19.1748ms, Mean +/- Std 19.1748ms +/- 0s
Sum of error:-6.935119145623503e-12
Time after second call.
Overall summary: Max 29.0962ms, Min 5.7848ms, Mean +/- Std 7.69065ms +/- 4.21244ms
Top 10 summary: Max 5.92441ms, Min 5.7848ms, Mean +/- Std 5.87683ms +/- 44.1624µs
今回は線形補間処理をTensorflowの行列計算で実装する方法を紹介しましたが,如何でしょうか?
最初はさらっと書くつもりだったのですが,Tensorflowの実装でハマったところなどをちょこちょこ入れていったら結局長くなってしまいました(^^;).
今回実装した関数はTensorflow Liteにコンバートして動かすことも可能です.
使い道があれば,組み込み機器に実装して動かしてみるのも面白そうです.
今回紹介した話が,補間処理などでお悩みの方に何か参考になれば幸いです.