目次
こんにちは.高山です.
先日の記事で告知しました手話入門記事の第十三回になります.
今回は手話動画から抽出した追跡点系列に対して,データ拡張を施すことで認識性能を改善する手法を紹介します.
具体的には,手話中の追跡点系列を時間軸方向に伸縮して (ワーピングと言います),様々な速度の手話データを生成する手法を紹介します.
図1にワーピングの適用例を示します.
図1(b) では,追跡点系列を2倍の長さにしています.
(つまり,手話の速度は半分になっています)
一般的に,手話動作は話者や状況によって速さが変化します.
手話の母語話者と学習者では手話の速度は大きく異なります.
また,テキパキした性格の方は手話が速く,おっとりした性格の方は手話がゆっくりであると聞いています.
(高山の手話力では,どちらの方も速く感じますが(^^;))
急いでいる状況では,当然手話は速くなるでしょう.
また,撮影環境によってもデータ上の手話速度は変化します.
手話動画を 60 frames per second (FPS) で撮影したデータは,30 FPS で撮影したデータの倍のフレーム数があります.
事前にフレーム数を揃える処理をしない場合,認識モデルから見ると30 FPS のデータは 60 FPS の倍の速度で手話を行っているように見えます.
認識モデルは,入力される手話動作の速度に依存せず,適切に動作を認識する必要があります.
しかしながら,学習データに含まれている手話動作は撮影環境や話者が限定されていることが多く,バリエーションが十分ではありません.
ワーピングにより手話速度のバリエーションを増やすことで,手話動作の速度変化に対して頑健になることが期待できます.
今回解説するスクリプトはGitHub上に公開しています.
複数の実験を行っている都合で,CPUで動かした場合は結構時間がかるのでご注意ください.
更新履歴 (大きな変更のみ記載しています)
- 2024/09/18: カテゴリを変更しました
- 2024/09/17: タグを更新しました
- 2024/07/29: Gitスクリプトのダウンロード元を
master
からv0.1
タグに変更 - 2024/07/23
- 第1節の構成を見直し
- 記事最終部の実験結果を削除して第2節に統合
1. 追跡点系列のワーピング処理
1.1 処理工程
図2に,追跡点系列のワーピング処理工程を示します.
処理は,ワーピング後の長さの算出,ワーピングの適用,および (オプションで) 追跡失敗フレームのマスキング処理から構成されます.
ワーピング後の長さをランダムなパラメータで算出することで,毎回異なる長さの学習データを生成できます.
図2中央に,ワーピング前後でデータがどのように変化するかを示します.
ここでは,時系列データに対する処理が分かりやすいように追跡点系列を画像形式で表しています.
横軸はフレーム数を示し,縦軸は追跡点番号を示します.
各ピクセルの色 (値) は追跡点の \(x\) 座標値を示しています.
このように画像形式で表した特徴量は,特徴マップと呼ばれます.
特徴マップにおいては,ワーピングは横方向に画像を伸縮する操作に相当します.
もし追跡点や時系列データの処理で悩んでいることがあれば,特徴マップ上の画像処理として捉えてあげると分かりやすくなるかもしれません.
1.2 線形補間と疑似信号のマスキング
ワーピング処理の実装には様々なアルゴリズムが考えられますが,今回は線形補間を用います.
線形補間の実装については以前の記事で詳しく説明しておりますので,併せてご一読いただければうれしいです.
線形補間を使っている都合上,ワーピング後に追跡失敗フレームと追跡成功フレームの間に疑似信号が発生する場合があります.
マスキングは上記の疑似信号を除去する処理です.
疑似信号とそのマスキング例を図3に示します.
図3(a)に示すように,マスキングをしない場合は疑似信号の影響で左手が原点から移動しているようなデータになってしまっていることが分かります.
(正規化後の系列では,左手が原点を中心に収縮するようなデータになります)
ただし,疑似信号が発生するフレーム数は相対的に少ないです.
また,疑似信号が認識に悪影響を及ぼす場合,Attentionはその影響を緩和するように働くことが期待できます.
実際の認識ではそこまで気にしすぎなくても良いかもしれません.
2. 実験結果
次節以降では,いつも通り実装の紹介をしながら実験結果をお見せします.
コード紹介記事の方針として記事単体で全処理が分かるように書いており,少し長いので結果を先にお見せしたいと思います.
図5は,データ拡張が無い場合,ワーピングを適用した場合,およびクリッピングとワーピングを適用した場合の,Validation Lossと認識率の推移を示しています.
横軸は学習・評価ループの繰り返し数 (Epoch) を示します.
縦軸はそれぞれの評価指標を示します.
各線の色と実験条件の関係は次のとおりです.
- 青線 (Default): Pre-LN構成のTransformer
- 橙線 (+ T-Warp): 時系列ワーピング処理適用
- 緑線 (+ T-Clip & T-Warp): 時系列クリッピング & 時系列ワーピング処理適用
デフォルトのモデルには,第九回の記事で紹介した,Pre-LN構成のTransformerモデルを用います.
時系列クリッピングは,第十二回の記事で紹介したデータ拡張手法で,時系列を任意の時間範囲で切り抜く手法です.
時系列ワーピングと組み合わせることで,より複雑なデータを生成することができます.
青線と橙線の比較結果から,安定してというほどではないですが,時系列ワーピングを適用することでロスの値,認識性能ともに改善していることが分かります.
時系列クリッピングと時系列ワーピングの組み合わせ (緑線) は,データが複雑な分最初は伸び悩んでいますが,40エポック付近から認識性能が改善していっています.
追加学習をすれば他を上回る性能が期待できそうですね.
なお,線形補間後のマスキングをしない場合の認識性能も確認しましたが,目立った違いが観測できなかったので,ここでは割愛しています.
今回の実験では話を簡単にするために,実験条件以外のパラメータは固定にし,乱数の制御もしていません.
必ずしも同様の結果になるわけではないので,ご了承ください.
3. 前準備
3.1 データセットのダウンロード
ここからは実装方法の説明をしていきます.
まずは,前準備としてGoogle Colabにデータセットをアップロードします.
ここの工程はこれまでの記事と同じですので,既に行ったことのある方は第3.3項まで飛ばしていただいて構いません.
まず最初に,データセットの格納先からデータをダウンロードし,ご自分のGoogle driveへアップロードしてください.
次のコードでGoogle driveをColabへマウントします.
Google Driveのマウント方法については,補足記事にも記載してあります.
1 2 3 |
|
ドライブ内のファイルをColabへコピーします.
パスはアップロード先を設定する必要があります.
# Copy to local.
!cp [path_to_dataset]/gislr_dataset_top10.zip gislr_top10.zip
データセットはZIP形式になっているので unzip
コマンドで解凍します.
!unzip gislr_top10.zip
Archive: gislr_top10.zip
creating: dataset_top10/
inflating: dataset_top10/16069.hdf5
...
inflating: dataset_top10/sign_to_prediction_index_map.json
成功すると dataset_top10
以下にデータが解凍されます.
HDF5ファイルはデータ本体で,手話者毎にファイルが別れています.
JSONファイルは辞書ファイルで,TXTファイルは本データセットのライセンスです.
!ls dataset_top10
16069.hdf5 25571.hdf5 29302.hdf5 36257.hdf5 49445.hdf5 62590.hdf5
18796.hdf5 26734.hdf5 30680.hdf5 37055.hdf5 53618.hdf5 LICENSE.txt
2044.hdf5 27610.hdf5 32319.hdf5 37779.hdf5 55372.hdf5 sign_to_prediction_index_map.json
22343.hdf5 28656.hdf5 34503.hdf5 4718.hdf5 61333.hdf5
単語辞書には単語名と数値の関係が10単語分定義されています.
!cat dataset_top10/sign_to_prediction_index_map.json
{
"listen": 0,
"look": 1,
"shhh": 2,
"donkey": 3,
"mouse": 4,
"duck": 5,
"uncle": 6,
"hear": 7,
"pretend": 8,
"cow": 9
}
ライセンスはオリジナルと同様に,CC-BY 4.0 としています.
!cat dataset_top10/LICENSE.txt
The dataset provided by Natsuki Takayama (Takayama Research and Development Office) is licensed under CC-BY 4.0.
Author: Copyright 2024 Natsuki Takayama
Title: GISLR Top 10 dataset
Original licenser: Deaf Professional Arts Network and the Georgia Institute of Technology
Modification
- Extract 10 most frequent words.
- Packaged into HDF5 format.
次のコードでサンプルを確認します.
サンプルは辞書型のようにキーバリュー形式で保存されており,下記のように階層化されています.
- サンプルID (トップ階層のKey)
|- feature: 入力特徴量で `[C(=3), T, J(=543)]` 形状.C,T,Jは,それぞれ特徴次元,フレーム数,追跡点数です.
|- token: 単語ラベル値で `[1]` 形状.0から9の数値です.
1 2 3 4 5 6 7 8 9 |
|
['1109479272', '11121526', ..., '976754415']
<KeysViewHDF5 ['feature', 'token']>
(3, 23, 543)
[1]
3.2 モジュールのダウンロード
次に,過去の記事で実装したコードをダウンロードします.
本項は前回までに紹介した内容と同じですので,飛ばしていただいても構いません.
コードはGithubのsrc/modules_gislr
にアップしてあります (今後の記事で使用するコードも含まれています).
まず,下記のコマンドでレポジトリをダウンロードします.
(目的のディレクトリだけダウンロードする方法はまだ調査中です(^^;))
!wget https://github.com/takayama-rado/trado_samples/archive/refs/tags/v0.1.zip -O master.zip
--2024-01-21 11:01:47-- https://github.com/takayama-rado/trado_samples/archive/master.zip
Resolving github.com (github.com)... 140.82.112.3
...
2024-01-21 11:01:51 (19.4 MB/s) - ‘master.zip’ saved [75710869]
ダウンロードしたリポジトリを解凍します.
!unzip -o master.zip -d master
Archive: master.zip
641b06a0ca7f5430a945a53b4825e22b5f3b8eb6
creating: master/trado_samples-main/
inflating: master/trado_samples-main/.gitignore
...
モジュールのディレクトリをカレントディレクトリに移動します.
!mv master/trado_samples-main/src/modules_gislr .
他のファイルは不要なので削除します.
!rm -rf master master.zip gislr_top10.zip
!ls
dataset_top10 drive modules_gislr sample_data
3.3 モジュールのロード
主要な処理の実装に先立って,下記のコードでモジュールをロードします.
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 45 46 47 48 49 50 |
|
【コード解説】
- 標準モジュール
- copy: データコピーライブラリ.Transformerブロック内でEncoder層をコピーするために使用します.
- json: JSONファイル制御ライブラリ.辞書ファイルのロードに使用します.
- math: 数学計算処理ライブラリ
- os: システム処理ライブラリ
- random: ランダム値生成ライブラリ
- sys: Pythonインタプリタの制御ライブラリ.
今回はローカルモジュールに対してパスを通すために使用します.
- functools: 関数オブジェクトを操作するためのライブラリ.
今回はDataLoaderクラスに渡すパディング関数に対して設定値をセットするために使用します.
- inspect.signature: オブジェクトの情報取得ライブラリ.
- pathlib.Path: オブジェクト指向のファイルシステム機能.
主にファイルアクセスに使います.osモジュールを使っても同様の処理は可能です.
高山の好みでこちらのモジュールを使っています(^^;).
- typing: 関数などに型アノテーションを行う機能.
ここでは型を忘れやすい関数に付けていますが,本来は全てアノテーションをした方が良いでしょう(^^;).
- 3rdパーティモジュール
- numpy: 行列演算ライブラリ
- torch: ニューラルネットワークライブラリ
- torchvision: PyTorchと親和性が高い画像処理ライブラリ.
今回はDatasetクラスに与える前処理をパッケージするために用います.
- ローカルモジュール: sys.pathにパスを追加することでロード可能
- dataset: データセット操作用モジュール
- defines: 各部位の追跡点,追跡点間の接続関係,およびそれらへのアクセス処理を
定義したモジュール
- layers: ニューラルネットワークのモデルやレイヤモジュール
- transforms: 入出力変換処理モジュール
- train_functions: 学習・評価処理モジュール
4. 時系列ワーピング処理の実装
本節では,時系列ワーピング処理を実装します.
時系列ワーピング処理には,欠損処理の補間で用いた線形補間処理を応用します.
線形補間処理の実装は下記のとおりです.
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 |
|
【コード解説】
- 引数
- x: 補間後の (仮想) フレームインデクス.
`x` 内のフレームインデクス値は小数値を取り得ることに注意してください.
例えば,`x = [0.0, 0.5, 1.0, 1.5, ..., T - 1.0]` の場合,`0.0` や `1.0` に
対応する特徴量はオリジナルの値になり,`0.5` や `1.5` に対応する特徴量は補間された
値になります.
この例では結果的にワーピング後の系列は2倍の長さに伸展されます.
- xs: オリジナルの系列のフレームインデクス.
- ys: オリジナルの系列の特徴量系列.
並列処理のため,`[T, C*J]` 形状であることを想定しています.
- 1行目: 内部処理で用いるため,`ys` の形状情報を取得
- 7-8行目: 配列の型を揃える
- 10-11行目: `xs` と `ys` の先頭と末尾にダミー値を追加.
- 14-15行目: 直線の傾き行列を算出し,先頭にダミー値を追加.
- 18行目: 直線の切片行列を算出.
- 24-26行目: ルックアップテーブルを作成して,全フレームのインデクスに対する直線パラメータを取得.
- 29-31行目: 線形補間を実行し,型と形状を入力に合わせて返す
計算処理の細かな意図については,線形補間処理の記事をご参照ください.
次に,下記のコードでワーピング処理を実装します.
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
【コード解説】
- 引数
- apply_ratio: データ拡張の適用確率.
- scale_range: ワーピング後の長さの範囲 (係数).
2要素の配列で `(min, max)` のように指定します.
`scale_range` の範囲でランダムにワーピング長を算出します.
- min_apply_size: ワーピングを行う最短入力長.
- apply_post_mask: `True` の場合,後処理で追跡失敗フレームのマスク処理を行います.
- 7-10行目: 初期化処理
- 12-60行目: ワーピング処理
- 16-17行目: ランダムに $[0, 1]$ の値を取得し,`apply_ratio` より上の値の場合は何もしない
- 20-26行目: `apply_post_mask == True` の場合,追跡成功フレームが `1`,失敗フレームが `0`
となるマスキング配列を生成する
- 27-29行目: 入力系列が `min_apply_size` より長いか判定
- 31行目: 線形補間の並列処理のために,特徴量の形状を `[C, T, J] -> [T, C*J]` に変更
- 34-35行目: `scale_range` の範囲でランダムにワーピング後の長さを算出
- 37-39行目: 線形補間の適用
- 41-42行目: 特徴量形状を `[T, C*J] -> [C, T, J]` に変更
- 43-55行目: マスキング処理の適用.
- 45-47行目: 先に求めた `mask` を特徴量と同様に線形補間.
- 54-55行目: 補間されたマスキング値になっている箇所を `0` クリアし,マスクを適用.
`1.0` 未満で判定していない理由は,丸め誤差の影響で追跡成功フレームのマスキング値が
1にならない場合があるためです.
- 62-63行目: print()に対して,クラス名と設定値を返す
5. 認識モデルの動作確認
今回は,第九回の記事で紹介した,Pre-LN構成のTransformerモデルをそのまま用いて実験を行います.
ここではモデルの推論動作が正常に動くかだけ確かめます.
次のコードでデータセットからHDF5ファイルとJSONファイルのパスを読み込みます.
1 2 3 4 5 6 7 8 |
|
dataset_top10/sign_to_prediction_index_map.json
[PosixPath('dataset_top10/2044.hdf5'), PosixPath('dataset_top10/32319.hdf5'), PosixPath('dataset_top10/18796.hdf5'), PosixPath('dataset_top10/36257.hdf5'), PosixPath('dataset_top10/62590.hdf5'), PosixPath('dataset_top10/16069.hdf5'), PosixPath('dataset_top10/29302.hdf5'), PosixPath('dataset_top10/34503.hdf5'), PosixPath('dataset_top10/37055.hdf5'), PosixPath('dataset_top10/37779.hdf5'), PosixPath('dataset_top10/27610.hdf5'), PosixPath('dataset_top10/53618.hdf5'), PosixPath('dataset_top10/49445.hdf5'), PosixPath('dataset_top10/30680.hdf5'), PosixPath('dataset_top10/22343.hdf5'), PosixPath('dataset_top10/55372.hdf5'), PosixPath('dataset_top10/26734.hdf5'), PosixPath('dataset_top10/28656.hdf5'), PosixPath('dataset_top10/61333.hdf5'), PosixPath('dataset_top10/4718.hdf5'), PosixPath('dataset_top10/25571.hdf5')]
次のコードで辞書ファイルをロードして,認識対象の単語数を格納します.
1 2 3 4 5 |
|
次のコードで前処理を定義します.
固定の前処理には,以前に説明した追跡点の選定と,追跡点の正規化を適用して実験を行います.
データ拡張処理は,動的な前処理として transforms_w_daug_twap
(12-17行目,ワーピング処理) と transforms_w_daug_tclip_twap
(19-28行目,クリッピング処理とワーピング処理) に格納します.
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 |
|
次のコードで,前処理を適用したHDF5DatasetとDataLoaderをインスタンス化し,データを取り出します.
HDF5Dataset
をインスタンス化する際に,transforms
引数に transforms_w_daug_twarp
を渡してデータ拡張を有効にしています (10行目).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
torch.Size([2, 2, 24, 130])
次のコードでモデルをインスタンス化して,動作チェックをします.
追跡点抽出の結果,入力追跡点数は130で,各追跡点はXY座標値を持っていますので,入力次元数は260になります.
出力次元数は単語数なので10になります.
また,Transformer層の入力次元数は64に設定し,PFFN内部の拡張次元数は256に設定しています.
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 |
|
TransformerEnISLR(
(linear): Linear(in_features=260, out_features=64, bias=True)
(activation): ReLU()
(tr_encoder): TransformerEncoder(
(pos_encoder): PositionalEncoding(
(dropout): Dropout(p=0.1, inplace=False)
)
(layers): ModuleList(
(0-1): 2 x TransformerEncoderLayer(
(self_attn): MultiheadAttention(
(w_key): Linear(in_features=64, out_features=64, bias=True)
(w_value): Linear(in_features=64, out_features=64, bias=True)
(w_query): Linear(in_features=64, out_features=64, bias=True)
(w_out): Linear(in_features=64, out_features=64, bias=True)
(dropout_attn): Dropout(p=0.1, inplace=False)
)
(ffw): PositionwiseFeedForward(
(w_1): Linear(in_features=64, out_features=256, bias=True)
(w_2): Linear(in_features=256, out_features=64, bias=True)
(dropout): Dropout(p=0.1, inplace=False)
(activation): ReLU()
)
(dropout): Dropout(p=0.1, inplace=False)
(norm1): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
(norm2): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
)
)
(norm): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
)
(head): GPoolRecognitionHead(
(head): Linear(in_features=64, out_features=10, bias=True)
)
)
torch.Size([2, 10])
(2, 2, 24, 24) (2, 2, 24, 24)
6. 学習と評価
6.1 共通設定
では,実際に学習・評価を行います.
まずは,実験全体で共通して用いる設定値を次のコードで実装します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Using 2 cores for data loading.
Using cuda for computation.
6.2 学習・評価の実行
次のコードで学習・バリデーション・評価処理それぞれのためのDataLoaderクラスを作成します.
今回は,データ拡張処理の有無および種類による認識性能の違いを見たいので,実験毎にデータセットクラスをインスタンス化します.
1 2 3 4 5 6 7 8 9 10 11 |
|
次のコードでモデルをインスタンス化します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
TransformerEnISLR(
(linear): Linear(in_features=260, out_features=64, bias=True)
(activation): ReLU()
(tr_encoder): TransformerEncoder(
(pos_encoder): PositionalEncoding(
(dropout): Dropout(p=0.1, inplace=False)
)
(layers): ModuleList(
(0-1): 2 x TransformerEncoderLayer(
(self_attn): MultiheadAttention(
(w_key): Linear(in_features=64, out_features=64, bias=True)
(w_value): Linear(in_features=64, out_features=64, bias=True)
(w_query): Linear(in_features=64, out_features=64, bias=True)
(w_out): Linear(in_features=64, out_features=64, bias=True)
(dropout_attn): Dropout(p=0.1, inplace=False)
)
(ffw): PositionwiseFeedForward(
(w_1): Linear(in_features=64, out_features=256, bias=True)
(w_2): Linear(in_features=256, out_features=64, bias=True)
(dropout): Dropout(p=0.1, inplace=False)
(activation): ReLU()
)
(dropout): Dropout(p=0.1, inplace=False)
(norm1): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
(norm2): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
)
)
(norm): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
)
(head): GPoolRecognitionHead(
(head): Linear(in_features=64, out_features=10, bias=True)
)
)
次のコードで学習・評価処理を行います.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Start training.
--------------------------------------------------------------------------------
Epoch 1
Start training.
loss:6.061896 [ 0/ 3881]
loss:1.682878 [ 3200/ 3881]
Done. Time:6.310430824000008
Training performance:
Avg loss:2.242826
Start validation.
Done. Time:0.24587801900000272
Validation performance:
Avg loss:1.922268
Start evaluation.
Done. Time:1.2209089309999968
Test performance:
Accuracy:35.5%
--------------------------------------------------------------------------------
...
--------------------------------------------------------------------------------
Epoch 50
Start training.
loss:0.471832 [ 0/ 3881]
loss:0.180786 [ 3200/ 3881]
Done. Time:2.763131632000011
Training performance:
Avg loss:0.191776
Start validation.
Done. Time:0.23482673500001283
Validation performance:
Avg loss:1.137890
Start evaluation.
Done. Time:1.1628710510000246
Test performance:
Accuracy:68.5%
Minimum validation loss:0.7253884204796383 at 19 epoch.
Maximum accuracy:78.5 at 43 epoch.
以後,同様の処理を設定毎に繰り返します.
コード構成は同じですので,ここでは説明を割愛させていただきます.
また,この後グラフ等の描画も行っておりますが,本記事の主要点ではないため説明を割愛させていただきます.
なお,冒頭のメッセージは,マルチスレッドを fork
という方法で立ち上げた場合に出る警告のようです.
内部動作に寄るもので,ユーザ側からの対策方法は分かっていませんが,現象にご興味がある方はこちらのリンクをご参照ください.
今回は追跡点系列を時間軸に沿って伸縮することで,データ拡張を行う手法を紹介しましたが,如何でしたでしょうか?
以前にも少し触れましたが,データ拡張は様々な手法が提案されており,組み合わせることでより多彩なデータを作りだすことが可能です.
今回はクリッピングとワーピング処理の組み合わせを試しましたが,実際の研究開発ではより多くの手法を組み合わせていきます.
次回以降もデータ拡張手法を紹介していきますので,是非ご自身で色々な組み合わせを試してみてください.
今回紹介した話が,これから手話認識を勉強してみようとお考えの方に何か参考になれば幸いです.