手話認識入門15 - 様々な改善手法6: ノイズ付加によるデータ拡張

This image is generated with ChatGPT-4 Omni, and edited by the author.
作成日:2024年06月17日(月) 00:00
最終更新日:2024年10月24日(木) 18:25
カテゴリ:手話言語処理
タグ:  手話認識入門 孤立手話単語認識 データ拡張 Python

追跡点の座標値に対してノイズを加えることで,データ拡張を行う方法を紹介します.

こんにちは.高山です.
先日の記事で告知しました手話入門記事の第十五回になります.

今回は手話動画から抽出した追跡点系列に対して,データ拡張を施すことで認識性能を改善する手法を紹介します.
ただし,今回紹介する手法はこれまで紹介してきたデータ拡張と異なり,劣化させたデータを学習することで認識モデルの頑健性を向上させるアプローチになります.
具体的には,手話中の追跡点系列の座標値に対してノイズを加えることで,特徴量の分散を増やします.

図1にノイズ付加の適用例を示します.

(a): ノイズ付加前
(b): ノイズ付加後
ノイズ付加による追跡点系列の変換例

骨格追跡は,画像の解像度や明暗に影響を受けやすく,座標値が安定しない場合があります.
また,MediaPipeの全身追跡機能には複雑さの異なる3種類のモデルがあり,軽量モデルは他のモデルに比べて追跡点が細かく振動する現象が発生します.
このような現象はジッタ (Jitter) と呼ばれます.

図1(b) に示すように追跡点系列にノイズを加えることで,ジッタが発生している状況をシミュレーションすることが可能です.
(顔はかなり崩れてしまっていますが)

今回解説するスクリプトはGitHub上に公開しています
複数の実験を行っている都合で,CPUで動かした場合は結構時間がかるのでご注意ください.

更新履歴 (大きな変更のみ記載しています)

  • 2024/09/18: カテゴリを変更しました
  • 2024/09/17: タグを更新しました
  • 2024/07/29: Gitスクリプトのダウンロード元を master から v0.1タグに変更
  • 2024/07/23: 第1節の構成を見直し
  • 2024/07/20
    • 実験結果を更新し,第1.3項とコード解説 (第4節) を更新
    • 記事最終部の実験結果を削除して第1.3節に統合

1. ノイズ付加の処理工程

図2に,追跡点系列へのノイズ付加処理工程を示します.

ノイズ付加の処理工程を示すブロック図と,特徴マップがどのように変化するかを示す画像です.画像の後に説明があります.
ノイズ付加処理工程

処理は非常にシンプルで,ノイズスケールの算出とノイズ付加操作,およびマスキングから構成されます.
ノイズは一様分布やガウス分布など用いて,ランダムに値を生成することで作ることができます.
ノイズスケールは分布の幅を決めるパラメータ (例えば,ガウス分布の場合は標準偏差です) で,スケールをサンプル毎にランダムに決定することで様々な量のノイズを付加することができます.
マスキングは,追跡に失敗している点に加わったノイズを除去する操作です.

図2右側は,ノイズ付加前後の画像フレームと特徴マップを示しています.
ノイズの効果が分かりやすいように,ここでは大きめのノイズスケールを与えています.

画像フレームを見ると,ノイズ付加後は最早どのような手話をしているか判別できないほどに崩れています.
一方,特徴マップ上では大まかな特徴を保てていることが分かります.

認識モデルが実際に見ているのは特徴マップなので,このようなデータを入力しても上手く学習してくれます.
逆に言うと,画像フレームに示しているような情報を内部で生成して処理するわけではないので注意してください (そのような認識アプローチも実はあります [Duan'22]).

  • [Duan'22] H. Duan., et al. "Revisiting Skeleton-based Action Recognition," CVPR 2022.

2. 実験結果

次節以降では,いつも通り実装の紹介をしながら実験結果をお見せします.
コード紹介記事の方針として記事単体で全処理が分かるように書いており,少し長いので結果を先にお見せしたいと思います.

2.1 頻度の多い10単語を学習させた結果

ノイズ付加処理の実装ではアフィン変換 の時と同様に,下記のようなオプションが考えられます.

  • ノイズ付加のタイミング: 正規化前,正規化後
  • ノイズの与え方: 全身に同じパラメータ,部位毎に異なるパラメータ
  • ノイズスケールの算出方法: 与えた値をそのまま用いる,追跡点を囲む矩形に対する相対値
  • ノイズを生成する分布: 一様分布,ガウス分布

図3から図5で,ノイズ付加方法に応じたValidation Lossと認識率の推移を示します.

正規化前に全身に一括でノイズ付与

まず図3ではノイズを正規化前に全身に一括で与えた場合の,Validation Lossと認識率の推移を示しています.

10単語のデータセットを用いて,正規化前に全身に一括でノイズを付加した場合の,認識性能比較結果を図示したグラフです.画像前後の文章に詳細説明があります.
認識性能比較結果 (Top10, 全身一括, 正規化前)

横軸は学習・評価ループの繰り返し数 (Epoch) を示します.
縦軸はそれぞれの評価指標を示します.

各線の色と実験条件の関係は次のとおりです.

  • 青線 (Default): Pre-LN構成のTransformer (ノイズ無し)
  • 橙線 (+ PreW-A-U): 正規化前に全身に一括で適用.ノイズスケールは直接指定.ノイズ分布は一様分布.
  • 緑線 (+ PreW-A-G): 正規化前に全身に一括で適用.ノイズスケールは直接指定.ノイズ分布はガウス分布.
  • 赤線 (+ PreW-B-U): 正規化前に全身に一括で適用.ノイズスケールは矩形幅の相対値.ノイズ分布は一様分布.
  • 青線 (+ PreW-B-G): 正規化前に全身に一括で適用.ノイズスケールは矩形幅の相対値.ノイズ分布はガウス分布.

デフォルトのモデルには,第九回の記事で紹介した,Pre-LN構成のTransformerモデルを用います.

全体的にロスの挙動が不安定で,認識性能もあまり大きな違いは有りませんでした.

正規化後に全身に一括でノイズ付与

続いて図4ではノイズを正規化後に全身に一括で与えた場合の,Validation Lossと認識率の推移を示しています.

10単語のデータセットを用いて,正規化後に全身に一括でノイズを付加した場合の,認識性能比較結果を図示したグラフです.画像前後の文章に詳細説明があります.
認識性能比較結果 (Top10, 全身一括, 正規化後)

各線の色と実験条件の関係は次のとおりです.

  • 青線 (Default): Pre-LN構成のTransformer (ノイズ無し)
  • 橙線 (+ PostW-A-U): 正規化後に全身に一括で適用.ノイズスケールは直接指定.ノイズ分布は一様分布.
  • 緑線 (+ PostW-A-G): 正規化後に全身に一括で適用.ノイズスケールは直接指定.ノイズ分布はガウス分布.
  • 赤線 (+ PostW-B-U): 正規化後に全身に一括で適用.ノイズスケールは矩形幅の相対値.ノイズ分布は一様分布.
  • 青線 (+ PostW-B-G): 正規化後に全身に一括で適用.ノイズスケールは矩形幅の相対値.ノイズ分布はガウス分布.

ノイズスケールを直接指定しているケース (PostW-A-U と PostW-A-G) が微妙に良くなっている...かな...? という感じはありますが,全体的な傾向は図4と同じようです.

部位毎にノイズ付与

最後に図5でノイズを部位毎に与えた場合の,Validation Lossと認識率の推移を示しています.

10単語のデータセットを用いて,部位毎にでノイズを付加した場合の,認識性能比較結果を図示したグラフです.画像前後の文章に詳細説明があります.
認識性能比較結果 (Top10, 部位毎)

各線の色と実験条件の関係は次のとおりです.

  • 青線 (Default): Pre-LN構成のTransformer (ノイズ無し)
  • 橙線 (+ PreP-A-U): 正規化前に部位毎に適用.ノイズスケールは直接指定.ノイズ分布は一様分布.
  • 緑線 (+ PreP-A-G): 正規化前に部位毎に適用.ノイズスケールは直接指定.ノイズ分布はガウス分布.
  • 赤線 (+ PostP-B-U): 正規化後に部位毎に適用.ノイズスケールは矩形幅の相対値.ノイズ分布は一様分布.
  • 青線 (+ PostP-B-G): 正規化後に部位毎に適用.ノイズスケールは矩形幅の相対値.ノイズ分布はガウス分布.

なお,ノイズスケールを矩形幅の相対値にして部位毎に適用する場合は,正規化でノイズもスケールされます.
設定の違いがノイズ量に影響を与えなかったので PreP-B-* (正規化前に部位毎に適用.ノイズスケールは矩形幅の相対値.) などは省略しています.

PreP-A-U がやや良い結果に見えますね.

ここまでの結果をまとめると,ノイズスケールは直接指定をした方が良い結果が出る傾向があるように見えます.
全体的に不安定なため,正規化のタイミングや分布の選択は明確な差が有るようには見えませんでした.

2.2 250単語を学習させた結果

データが少なくて学習が不安定になっている可能性がありますので,全データ (250単語) を学習させた場合の挙動を図6から図8に示します.
なお,こちらの実験はメモリや処理時間の都合でColab上では実行が難しいので,ローカル環境で行いました.

データの分割方法やパラメータは10単語のときと同じです.
ただし,学習時間を短縮するためにバッチ数は256に設定しています.
(本来はバッチ数を変えた場合は学習率も調整した方が良いのですが,今回はママで実験を行っています)

正規化前に全身に一括でノイズ付与

まず図6ではノイズを正規化前に全身に一括で与えた場合の,Validation Lossと認識率の推移を示しています.

250単語のデータセットを用いて,正規化前に全身に一括でノイズを付加した場合の,認識性能比較結果を図示したグラフです.画像前後の文章に詳細説明があります.
認識性能比較結果 (Full, 全身一括, 正規化前)

ロスの挙動は安定していますね.
一方,認識性能は Default と同等か少し悪くなっているように見えます.

正規化後に全身に一括でノイズ付与

続いて図7ではノイズを正規化後に全身に一括で与えた場合の,Validation Lossと認識率の推移を示しています.

250単語のデータセットを用いて,正規化後に全身に一括でノイズを付加した場合の,認識性能比較結果を図示したグラフです.画像前後の文章に詳細説明があります.
認識性能比較結果 (Full, 全身一括, 正規化後)

こちらも図6と同様に,認識性能は Default と同等か少し悪くなっているようです.

部位毎にノイズ付与

最後に図8でノイズを部位毎に与えた場合の,Validation Lossと認識率の推移を示しています.

250単語のデータセットを用いて,部位毎にノイズを付加した場合の,認識性能比較結果を図示したグラフです.画像前後の文章に詳細説明があります.
認識性能比較結果 (Full, 部位毎)

ノイズを正規化前に与えるケース (PreP-A-U と PreP-A-G) が少し良い結果になっているようですね.

ノイズを部位毎に異なるパラメータで与える処理は汎化性能を向上する効果がありそうです.
一方,ノイズ分布の違いや,部位サイズに応じてノイズ量を調整する処理はそこまで効果が見られませんでした.

ここまでの実験結果から分かるとおり,ロス,認識性能共に大きな違いは出ませんでしたね...(^^;).
(図2の特徴マップで示された結果からある程度予想できてましたが...)

ただし今回は劣化したデータを学習させていることを考えると,認識性能を保てているという解釈もできるかもしれません.

なお,今回の実験では話を簡単にするために,実験条件以外のパラメータは固定にし,乱数の制御もしていません.
必ずしも同様の結果になるわけではないので,ご了承ください.

3. 前準備

3.1 データセットのダウンロード

ここからは実装方法の説明をしていきます.
まずは,前準備としてGoogle Colabにデータセットをアップロードします. ここの工程はこれまでの記事と同じですので,既に行ったことのある方は第3.3項まで飛ばしていただいて構いません.

まず最初に,データセットの格納先からデータをダウンロードし,ご自分のGoogle driveへアップロードしてください.

次のコードでGoogle driveをColabへマウントします.
Google Driveのマウント方法については,補足記事にも記載してあります.

1
2
3
from google.colab import drive

drive.mount("/content/drive")

ドライブ内のファイルを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
with h5py.File("dataset_top10/16069.hdf5", "r") as fread:
    keys = list(fread.keys())
    print(keys)
    group = fread[keys[0]]
    print(group.keys())
    feature = group["feature"][:]
    token = group["token"][:]
    print(feature.shape)
    print(token)
['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
import copy
import json
import math
import os
import random
import sys
from functools import partial
from inspect import signature
from pathlib import Path
from typing import (
    Any,
    Dict,
    List
)

# Third party's modules
import numpy as np

import torch
from torch import nn
from torch.nn import functional as F
from torch.utils.data import (
    DataLoader)

from torchvision.transforms import Compose

# Local modules
sys.path.append("modules_gislr")
from modules_gislr.dataset import (
    HDF5Dataset,
    merge_padded_batch)
from modules_gislr.defines import (
    get_fullbody_landmarks
)
from modules_gislr.layers import (
    Identity,
    GPoolRecognitionHead,
    TransformerEnISLR
)
from modules_gislr.train_functions import (
    test_loop,
    val_loop,
    train_loop
)
from modules_gislr.transforms import (
    PartsBasedNormalization,
    ReplaceNan,
    SelectLandmarksAndFeature,
    ToTensor
)
【コード解説】
- 標準モジュール
  - 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
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
64
65
66
67
68
69
70
71
72
class RandomNoise():
    def __init__(self,
                 apply_ratio,
                 scale_range,
                 scale_unit="asis",
                 noise_type="uniform",
                 target_joints=None,
                 feature_dim=3,
                 include_conf=True):
        assert noise_type in ["uniform", "gauss"]
        self.apply_ratio = apply_ratio
        self.scale_range = np.array(scale_range)
        self.scale_unit = scale_unit
        self.noise_type = noise_type
        self.feature_dim = feature_dim
        self.include_conf = include_conf
        self.target_joints = target_joints

    def __call__(self,
                 data: Dict[str, Any]) -> Dict[str, Any]:

        if random.random() > self.apply_ratio:
            return data

        feature = data["feature"]
        # Remove confidence if it's included.
        if self.include_conf:
            confs = feature[-1:, :, :]
            feature = feature[:-1, :, :]
        else:
            confs = None

        # `[C, T, J]`
        target = feature[:, :, self.target_joints]
        # Generate mask to prevent adding noise to failed tracking.
        temp = target.reshape([target.shape[0], -1])
        bmask = (temp == 0).all(axis=0)
        bmask = np.bitwise_not(bmask)
        if bmask.any() == np.False_:
            return data
        # Mask for post-process.
        mask = bmask.reshape(
            [1, target.shape[1], target.shape[2]]).astype(target.dtype)

        # Calculate real scale.
        if self.scale_unit == "box_ratio":
            mins = temp[:, bmask].min(axis=1)
            maxes = temp[:, bmask].max(axis=1)
            size = np.linalg.norm(maxes - mins)
            scale_range = size * self.scale_range
        else:
            scale_range = self.scale_range

        # Apply augmentation.
        aug_scale = np.random.random() * (scale_range[1] - scale_range[0]) + scale_range[0]
        if self.noise_type == "uniform":
            target += np.random.uniform(low=-aug_scale, high=aug_scale, size=target.shape)
        elif self.noise_type == "gauss":
            target += np.random.normal(loc=0.0, scale=aug_scale, size=target.shape)

        # Filter out failed tracking to zero.
        target *= mask
        feature[:, :, self.target_joints] = target
        # Back confidence.
        if confs is not None:
            feature = np.concatenate([feature, confs], axis=0)

        data["feature"] = feature
        return data

    def __str__(self):
        return f"{self.__class__.__name__}:{self.__dict__}"
【コード解説】
- 引数
  - apply_ratio: データ拡張の適用確率.
  - scale_range: ノイズスケールの範囲.
    2要素の配列で `(min, max)` のように指定します.
    `scale_range` の範囲でランダムにノイズスケール値を算出します.
    ただし,`scale_unit="box_ratio"` の場合は,`target_joints`を囲む矩形の
    対角線長を算出し,その長さに対する相対値に変換します.
  - scale_unit: ノイズスケールの算出方法を指定.
    - asis: `scale_range` の範囲でランダムに値を取り出し,そのまま使用します.
    - box_ratio: `target_joints`を囲む矩形の対角線長を算出し,`scale_range` を
      対角線長に対する相対値として見なして変換します.
  - noise_type: ノイズを生成する分布を指定.
    - uniform: 一様分布を用います.値は `(-scale, scale)` の範囲で取り出します.
    - gauss: ガウス分布を用います.値は平均0, 標準偏差 `scale` の分布から
      取り出します.
  - target_joints: 変換を適用する追跡点インデクス.
    部位毎に適用する場合は,ここの設定を変えたインスタンスを部位毎に生成する
    必要があります.
  - feature_dim: 特徴量の次元数
  - include_conf: Trueの場合,特徴量の末尾は信頼度の次元であるとみなし,
    処理を行います.
- 10-17行目: 初期化処理
- 19-69行目: ノイズ付加処理
  - 22-23行目: 乱数を生成し,`apply_ratio` 以上だった場合は何もせずに値を返す
  - 27-31行目: 信頼度にノイズが入らないように,配列を分離
  - 34行目: `target_joints` で指定した追跡点を抽出
  - 36-43行目: 追跡失敗フレームにノイズが入らないように,マスクを生成
  - 46-52行目: `scale_unit == "box_ratio"` の場合は,`scale_range` を変換
  - 55行目: ノイズスケールを算出
  - 56-59行目: 指定した分布でノイズを付加
  - 62行目: マスクの適用
  - 63-69行目: 特徴量を整形し返す
- 71-72行目: print()に対して,クラス名と設定値を返す

5. 認識モデルの動作確認

今回は,第九回の記事で紹介した,Pre-LN構成のTransformerモデルをそのまま用いて実験を行います.
ここではモデルの推論動作が正常に動くかだけ確かめます.

次のコードでデータセットからHDF5ファイルとJSONファイルのパスを読み込みます.

1
2
3
4
5
6
7
8
# Access check.
dataset_dir = Path("dataset_top10")
files = list(dataset_dir.iterdir())
dictionary = [fin for fin in files if ".json" in fin.name][0]
hdf5_files = [fin for fin in files if ".hdf5" in fin.name]

print(dictionary)
print(hdf5_files)
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
# Load dictionary.
with open(dictionary, "r") as fread:
    key2token = json.load(fread)

VOCAB = len(key2token)

次のコードで前処理を定義します.
固定の前処理には,以前に説明した追跡点の選定と,追跡点の正規化を適用して実験を行います.

第2節で述べたとおり,ノイズ付加処理の適用の仕方は様々な設定が考えられます. 今回は下記のコードに示すように,変数毎に異なる設定を定義しました (多数あるので,個々の解説は割愛します).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
_, use_landmarks = get_fullbody_landmarks()
use_features = ["x", "y"]
trans_select_feature = SelectLandmarksAndFeature(landmarks=use_landmarks, features=use_features)
trans_repnan = ReplaceNan()
trans_norm = PartsBasedNormalization(align_mode="framewise", scale_mode="unique")

apply_ratio = 0.5
scale_range = (1e-3, 1e-2)
target_joints = np.arange(0, len(use_landmarks))
feature_dim = len(use_features)
include_conf = False

pre_transforms = Compose([trans_select_feature,
                          trans_repnan])

transforms_default = Compose([
    trans_norm,
    ToTensor()])
 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
# Apply noise to whole joints before normalization.
transforms_snoise_prewhole_asis_uniform = Compose([
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="asis",
        noise_type="uniform",
        target_joints=target_joints,
        feature_dim=feature_dim,
        include_conf=include_conf),
    trans_norm,
    ToTensor()])

transforms_snoise_prewhole_asis_gauss = Compose([
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="asis",
        noise_type="gauss",
        target_joints=target_joints,
        feature_dim=feature_dim,
        include_conf=include_conf),
    trans_norm,
    ToTensor()])

transforms_snoise_prewhole_boxratio_uniform = Compose([
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type="uniform",
        target_joints=target_joints,
        feature_dim=feature_dim,
        include_conf=include_conf),
    trans_norm,
    ToTensor()])

transforms_snoise_prewhole_boxratio_gauss = Compose([
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type="gauss",
        target_joints=target_joints,
        feature_dim=feature_dim,
        include_conf=include_conf),
    trans_norm,
    ToTensor()])
 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
# Apply noise to whole joints after normalization.
transforms_snoise_postwhole_asis_uniform = Compose([
    trans_norm,
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="asis",
        noise_type="uniform",
        target_joints=target_joints,
        feature_dim=feature_dim,
        include_conf=include_conf),
    ToTensor()])

transforms_snoise_postwhole_asis_gauss = Compose([
    trans_norm,
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="asis",
        noise_type="gauss",
        target_joints=target_joints,
        feature_dim=feature_dim,
        include_conf=include_conf),
    ToTensor()])

transforms_snoise_postwhole_boxratio_uniform = Compose([
    trans_norm,
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type="uniform",
        target_joints=target_joints,
        feature_dim=feature_dim,
        include_conf=include_conf),
    ToTensor()])

transforms_snoise_postwhole_boxratio_gauss = Compose([
    trans_norm,
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type="gauss",
        target_joints=target_joints,
        feature_dim=feature_dim,
        include_conf=include_conf),
    ToTensor()])
  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
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# Apply noise to each parts.
target_joints = [
    np.arange(0, 76),  # Face
    np.arange(76, 76+21),  # LHand
    np.arange(76+21, 76+21+12),  # Pose
    np.arange(76+21+12, 76+21+12+21)  # RHand
]

# Before normalization.
noise_type = "uniform"
transforms_snoise_preparts_boxratio_uniform = Compose([
    # Face.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[0],
        feature_dim=feature_dim,
        include_conf=include_conf),
    # LHand.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[1],
        feature_dim=feature_dim,
        include_conf=include_conf),
    # Pose.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[2],
        feature_dim=feature_dim,
        include_conf=include_conf),
    # Rhand.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[3],
        feature_dim=feature_dim,
        include_conf=include_conf),
    trans_norm,
    ToTensor()])

noise_type = "gauss"
transforms_snoise_preparts_boxratio_gauss = Compose([
    # Face.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[0],
        feature_dim=feature_dim,
        include_conf=include_conf),
    # LHand.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[1],
        feature_dim=feature_dim,
        include_conf=include_conf),
    # Pose.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[2],
        feature_dim=feature_dim,
        include_conf=include_conf),
    # Rhand.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[3],
        feature_dim=feature_dim,
        include_conf=include_conf),
    trans_norm,
    ToTensor()])


# After normalization.
noise_type = "uniform"
transforms_snoise_postparts_boxratio_uniform = Compose([
    trans_norm,
    # Face.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[0],
        feature_dim=feature_dim,
        include_conf=include_conf),
    # LHand.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[1],
        feature_dim=feature_dim,
        include_conf=include_conf),
    # Pose.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[2],
        feature_dim=feature_dim,
        include_conf=include_conf),
    # Rhand.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[3],
        feature_dim=feature_dim,
        include_conf=include_conf),
    ToTensor()])

noise_type = "gauss"
transforms_snoise_postparts_boxratio_gauss = Compose([
    trans_norm,
    # Face.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[0],
        feature_dim=feature_dim,
        include_conf=include_conf),
    # LHand.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[1],
        feature_dim=feature_dim,
        include_conf=include_conf),
    # Pose.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[2],
        feature_dim=feature_dim,
        include_conf=include_conf),
    # Rhand.
    RandomNoise(
        apply_ratio=apply_ratio,
        scale_range=scale_range,
        scale_unit="box_ratio",
        noise_type=noise_type,
        target_joints=target_joints[3],
        feature_dim=feature_dim,
        include_conf=include_conf),
    ToTensor()])

次のコードで,前処理を適用したHDF5DatasetとDataLoaderをインスタンス化し,データを取り出します.
HDF5Dataset をインスタンス化する際に,pre_transformstransforms 引数に値を渡してデータ拡張を有効にしています (14行目).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
batch_size = 2
feature_shape = (len(use_features), -1, len(use_landmarks))
token_shape = (1,)
merge_fn = partial(merge_padded_batch,
                   feature_shape=feature_shape,
                   token_shape=token_shape,
                   feature_padding_val=0.0,
                   token_padding_val=0)

for trans in [transforms_default,
              transforms_snoise_prewhole_asis_uniform, transforms_snoise_prewhole_asis_gauss, transforms_snoise_prewhole_boxratio_uniform, transforms_snoise_prewhole_boxratio_gauss,
              transforms_snoise_postwhole_asis_uniform, transforms_snoise_postwhole_asis_gauss, transforms_snoise_postwhole_boxratio_uniform, transforms_snoise_postwhole_boxratio_gauss,
              transforms_snoise_preparts_boxratio_uniform, transforms_snoise_preparts_boxratio_gauss, transforms_snoise_postparts_boxratio_uniform, transforms_snoise_postparts_boxratio_gauss]:
    dataset = HDF5Dataset(hdf5_files, pre_transforms=pre_transforms, transforms=trans)
    dataloader = DataLoader(dataset, batch_size=batch_size, collate_fn=merge_fn)
    try:
        data = next(iter(dataloader))
        feature_origin = data["feature"]

        print(feature_origin.shape)
    except Exception as inst:
        print(inst)
torch.Size([2, 2, 232, 130])
torch.Size([2, 2, 232, 130])
torch.Size([2, 2, 232, 130])
torch.Size([2, 2, 232, 130])
torch.Size([2, 2, 232, 130])
torch.Size([2, 2, 232, 130])
torch.Size([2, 2, 232, 130])
torch.Size([2, 2, 232, 130])
torch.Size([2, 2, 232, 130])
torch.Size([2, 2, 232, 130])
torch.Size([2, 2, 232, 130])
torch.Size([2, 2, 232, 130])
torch.Size([2, 2, 232, 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
# Define model.
# in_channels: J * C (130*2=260)
#   J: use_landmarks (130)
#   C: use_channels (2)
# out_channels: 10
in_channels = len(use_landmarks) * len(use_features)
inter_channels = 64
out_channels = VOCAB
activation = "relu"
tren_num_layers = 2
tren_num_heads = 2
tren_dim_ffw = 256
tren_dropout_pe = 0.1
tren_dropout = 0.1
tren_layer_norm_eps = 1e-5
tren_norm_first = True
tren_add_bias = True
tren_add_tailnorm = True

model = TransformerEnISLR(in_channels=in_channels,
                          inter_channels=inter_channels,
                          out_channels=out_channels,
                          activation=activation,
                          tren_num_layers=tren_num_layers,
                          tren_num_heads=tren_num_heads,
                          tren_dim_ffw=tren_dim_ffw,
                          tren_dropout_pe=tren_dropout_pe,
                          tren_dropout=tren_dropout,
                          tren_layer_norm_eps=tren_layer_norm_eps,
                          tren_norm_first=tren_norm_first,
                          tren_add_bias=tren_add_bias,
                          tren_add_tailnorm=tren_add_tailnorm)
print(model)

# Sanity check.
logit = model(feature_origin)
print(logit.shape)
attw0 = model.tr_encoder.layers[0].attw.detach().cpu().numpy()
attw1 = model.tr_encoder.layers[0].attw.detach().cpu().numpy()
print(attw0.shape, attw1.shape)
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, 75, 75) (2, 2, 75, 75)

6. 学習と評価

6.1 共通設定

では,実際に学習・評価を行います.
まずは,実験全体で共通して用いる設定値を次のコードで実装します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Set common parameters.
batch_size = 32
load_into_ram = True
test_pid = 16069
num_workers = os.cpu_count()
print(f"Using {num_workers} cores for data loading.")
lr = 3e-4

epochs = 50
eval_every_n_epochs = 1
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using {device} for computation.")

train_hdf5files = [fin for fin in hdf5_files if str(test_pid) not in fin.name]
val_hdf5files = [fin for fin in hdf5_files if str(test_pid) in fin.name]
test_hdf5files = [fin for fin in hdf5_files if str(test_pid) in fin.name]

_, use_landmarks = get_fullbody_landmarks()
use_features = ["x", "y"]
Using 2 cores for data loading.
Using cuda for computation.

6.2 学習・評価の実行

次のコードで学習・バリデーション・評価処理それぞれのためのDataLoaderクラスを作成します.
今回は,ノイズ付加処理の有無に寄る認識性能の違いを見たいので,実験毎にデータセットクラスをインスタンス化します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Build dataloaders.
train_dataset = HDF5Dataset(train_hdf5files, pre_transforms=pre_transforms,
    transforms=transforms, load_into_ram=load_into_ram)
val_dataset = HDF5Dataset(val_hdf5files, pre_transforms=pre_transforms,
    transforms=transforms, load_into_ram=load_into_ram)
test_dataset = HDF5Dataset(test_hdf5files, pre_transforms=pre_transforms,
    transforms=transforms, load_into_ram=load_into_ram)

train_dataloader = DataLoader(train_dataset, batch_size=batch_size, collate_fn=merge_fn, num_workers=num_workers, shuffle=True)
val_dataloader = DataLoader(val_dataset, batch_size=batch_size, collate_fn=merge_fn, num_workers=num_workers, shuffle=False)
test_dataloader = DataLoader(test_dataset, batch_size=1, collate_fn=merge_fn, num_workers=num_workers, shuffle=False)

次のコードでモデルをインスタンス化します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
tren_norm_first = True
tren_add_tailnorm = True

model_default = TransformerEnISLR(
    in_channels=in_channels,
    inter_channels=inter_channels,
    out_channels=out_channels,
    activation=activation,
    tren_num_layers=tren_num_layers,
    tren_num_heads=tren_num_heads,
    tren_dim_ffw=tren_dim_ffw,
    tren_dropout_pe=tren_dropout_pe,
    tren_dropout=tren_dropout,
    tren_layer_norm_eps=tren_layer_norm_eps,
    tren_norm_first=tren_norm_first,
    tren_add_bias=tren_add_bias,
    tren_add_tailnorm=tren_add_tailnorm)
print(model_default)

loss_fn = nn.CrossEntropyLoss(reduction="mean")
optimizer = torch.optim.Adam(model_default.parameters(), lr=lr)
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
# Train, validation, and evaluation.
model_default.to(device)

train_losses = []
val_losses = []
test_accs = []
print("Start training.")
for epoch in range(epochs):
    print("-" * 80)
    print(f"Epoch {epoch+1}")

    train_losses = train_loop(train_dataloader, model_default, loss_fn, optimizer, device)
    val_loss = val_loop(val_dataloader, model_default, loss_fn, device)
    val_losses.append(val_loss)

    if (epoch+1) % eval_every_n_epochs == 0:
        acc = test_loop(test_dataloader, model_default, device)
        test_accs.append(acc)
train_losses_default = np.array(train_losses)
val_losses_default = np.array(val_losses)
test_accs_default = np.array(test_accs)

print(f"Minimum validation loss:{val_losses_default.min()} at {np.argmin(val_losses_default)+1} epoch.")
print(f"Maximum accuracy:{test_accs_default.max()} at {np.argmax(test_accs_default)*eval_every_n_epochs+1} epoch.")
Start training.
--------------------------------------------------------------------------------
Epoch 1
Start training.
loss:3.729503 [    0/ 3881]
loss:1.974881 [ 3200/ 3881]
Done. Time:8.072904322999989
Training performance: 
 Avg loss:2.244985

Start validation.
Done. Time:0.33059089099998573
Validation performance: 
 Avg loss:2.115503

Start evaluation.
Done. Time:1.345861483999954
Test performance: 
 Accuracy:18.5%
--------------------------------------------------------------------------------
...
--------------------------------------------------------------------------------
Epoch 50
Start training.
loss:0.218751 [    0/ 3881]
loss:0.133565 [ 3200/ 3881]
Done. Time:4.735246651000011
Training performance: 
 Avg loss:0.198414

Start validation.
Done. Time:0.33649530800005323
Validation performance: 
 Avg loss:0.945739

Start evaluation.
Done. Time:1.2618015409999543
Test performance: 
 Accuracy:76.0%
Minimum validation loss:0.7086029521056584 at 25 epoch.
Maximum accuracy:79.0 at 43 epoch.

以後,同様の処理を設定毎に繰り返します.
コード構成は同じですので,ここでは説明を割愛させていただきます. また,この後グラフ等の描画も行っておりますが,本記事の主要点ではないため説明を割愛させていただきます.


今回は追跡点系列に対してノイズを加えて,データ拡張を行う手法を紹介しましたが,如何でしたでしょうか?
ノイズを加えた場合に特性があまり変わらなかったので,解釈に時間がかかってしまいました(^^;)

今回使用したデータセットでは試せませんでしたが,オリジナルの動画がある場合は,ノイズが起きやすい環境や追跡モデルを使用したデータをテストデータとすることで,ノイズに対する頑健性を検証することができます.
他にも,テストデータにノイズを加えて検証をしてみるのも面白いかも知れませんね.
機会があれば試してみたいと思います.

今回紹介した話が,これから手話認識を勉強してみようとお考えの方に何か参考になれば幸いです.