Google American Sign Language Fingerspelling Recognitionのデータセットについて解説します

Thumbnail image
This image is generated with ChatGPT-4 Omni, and edited by the author.
作成日: 2024年09月08日(日) 00:00
最終更新日: 2024年09月18日(水) 09:54
カテゴリ: Kaggle
タグ:  手話認識 連続指文字認識 GAFS データセット Python

Google American Sign Language Fingerspelling Recognitionで配布される,連続指文字認識用データセットについて解説します.

こんにちは.高山です.
これまで様々な手話認識に関する記事を書いてきました.
今まで書いた記事は全て孤立手話単語認識の話題で,こちらはある程度充実してきましたので,そろそろ他の種類のタスクや技術も紹介したいと思っています.

そこで今回は,Kaggle の Google American Sign Language Fingerspelling Recognition (以下,GAFS) で用いられたデータセットについてお話したいと思います.

記事の後半では,孤立手話単語認識の時と同様に,GAFSデータセットを実験用にHDF5形式にまとめる方法を説明します.
今回解説するスクリプトはGitHub上に公開しています
Colab上で使用しているデータセットは,説明用に大部分の追跡点を削除したデータセットです.
ご自分の環境で試す場合は,オリジナルのデータセットをローカル環境で (Google Driveに入り切らないため) 使用するようにしてください.

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

  • 2024/09/17: タグを更新しました
  • 2024/09/08: HDF5ファイルをチャネルファースト形式で保存するように処理を修正しました.

1. GAFSコンペティションについて

Kaggleの参加記事でも紹介させていただきましたが,GAFSコンペティションでは,アメリカ手話 (American Sign Language: ASL) の中で使われる指文字を題材として,連続指文字認識の性能を競い合います.
このコンペティションでは認識モデルの学習・検証用として,下記のようなデータセットが配布されます.

  • ASL指文字数: 59文字 (アルファベット,数字,記号,スペース)
  • ASL話者数: 94名 (学習データセットに含まれる人数です.追加データ分,主催者側テスト分は含んでいません)
  • 入力データ: MediaPipe (version 0.9.0.1) による追跡点系列
  • 出力形式: 指文字で表現されている文字列を出力します.
    文字列の内容は,特定のフレーズ,住所,電話番号,およびURLなどです.
  • 総データ数: 67208個 (学習データセットに含まれる数です.追加データ分,主催者側テスト分は含んでいません)
  • ライセンスおよび注意事項: CC-BY 4.0.ただし,個人識別に用いることはできません.
  • ライセンサ: Google (データ提供には Deaf Professional Arts Network, the Georgia Institute of Technology, および National Technical Institute for the Deaf が関わっています)

コンペティションは既に終了していますが,データセットの入手および,テストデータを用いたモデル評価は今でも行なえます.
ただし,モデルはTensorflow Liteの形式で提出する必要があります.
また,モデルは規定以上のレイテンシでデータを処理できないといけません.

詳細については,公式サイトをご参照ください.

2. データセットについて

2.1 入手方法

図1に示すように,今回紹介するデータセットは Kaggle 内のコンペティションページからダウンロードできます.
全体をダウンロードすると,158GB 程度のサイズ (^^;) になりますので注意してください.
また,ダウンロードには Kaggle のアカウントが必要です.

図1: データセットの入手
データセットの入手

全体のダウンロードが完了すると,asl-fingerspelling.zip という ZIPファイルが取得できます.

2.2 データの内容

ZIPファイルを解凍すると,下記のようなファイルが展開されます.

$ unzip asl-fingerspelling.zip -d asl-fingerspelling
$ ls asl-fingerspelling
character_to_prediction_index.json  supplemental_metadata.csv  train_landmarks
supplemental_landmarks  train.csv

辞書ファイル

character_to_prediction_index.json は辞書ファイルで,次のように指文字名と数値の関係が定義されています.

$ cat character_to_prediction_index.json
{
    " ":0,
    "!":1,
    "#":2,
    "$":3,
    ...
    "~":58
}

表示内容から分かるように,59種類の指文字が 0 始まりの番号で定義されています.
最初の文字はスペース (半角の空白) です (プログラムで表示する際などは注意してください).

認識モデルは追跡点を入力した際に,辞書ファイルで定義した数値を出力するように実装する必要があります.
例えば,入力が abc という文字列を表している場合,認識モデルは 32, 33, 34 という数値列を返すように作らなければなりません.

サンプル情報

train.csv は各サンプルの情報を表形式でまとめたデータで,下記のような内容になっています.

                                     path     file_id  sequence_id  participant_id                          phrase
0         train_landmarks/5414471.parquet     5414471   1816796431             217                    3 creekhouse
1         train_landmarks/5414471.parquet     5414471   1816825349             107                 scales/kuhaylah
2         train_landmarks/5414471.parquet     5414471   1816909464               1             1383 william lanier
3         train_landmarks/5414471.parquet     5414471   1816967051              63               988 franklin lane
4         train_landmarks/5414471.parquet     5414471   1817123330              89       6920 northeast 661st road
...                                   ...         ...          ...             ...                             ...
67203  train_landmarks/2118949241.parquet  2118949241    388192924              88                    431-366-2913
67204  train_landmarks/2118949241.parquet  2118949241    388225542             154                    994-392-3850
67205  train_landmarks/2118949241.parquet  2118949241    388232076              95  https://www.tianjiagenomes.com
67206  train_landmarks/2118949241.parquet  2118949241    388235284              36               90 kerwood circle
67207  train_landmarks/2118949241.parquet  2118949241    388332538             176                      802 co 66b

各カラムの内容は次のとおりです.

  • path: 骨格追跡点データへのパス
  • file_id: ファイル番号
  • sequence_id: データ番号
  • participant_id: 指文字話者ID
  • phrase: 何の文字列を表出しているか

追跡点データ

train_landmarks は追跡点データが格納されているディレクトリです.

ls train_landmarks
1019715464.parquet
...
654436541.parquet

各ファイルは Apatche Parquet 形式で格納されており,Python の場合は Pandas を用いてロードすることが可能です.
格納されているデータ例を下記に示します.

import pandas as pd

df = pd.read_parquet("train_landmarks/450474571.parquet")
print(df)
             frame  x_face_0  x_face_1  ...  z_right_hand_19  z_right_hand_20
sequence_id
2138557488       0  0.663001  0.644330  ...        -0.117422        -0.111925
2138557488       1  0.667972  0.643810  ...        -0.114211        -0.101464
2138557488       2  0.671009  0.642717  ...              NaN              NaN
2138557488       3  0.668623  0.641970  ...              NaN              NaN
2138557488       4  0.669397  0.647073  ...              NaN              NaN
...            ...       ...       ...  ...              ...              ...
2147465106     231  0.652571  0.639059  ...        -0.119032        -0.097088
2147465106     232  0.659162  0.641239  ...        -0.023387        -0.012794
2147465106     233  0.663675  0.649184  ...        -0.070979        -0.060423
2147465106     234  0.662469  0.648533  ...        -0.096455        -0.080358
2147465106     235  0.660208  0.641396  ...        -0.133947        -0.112528

上に示すように,データは表形式で格納されており内容は次のとおりです.

  • index (sequence_id): データ番号.上の例で示されるとおり,1個のファイルに複数のデータがまとめられています.
  • frame: 追跡点系列のフレーム番号.評価で用いられるコードを見る限りでは,このカラムの情報は使われていないようです.
  • [x/y/z]_[type]_[landmark_index]: 追跡点の座標値.顔の x座標,左手の x座標,身体の x座標,右手の x座標,顔の y座標,...,右手の z座標の順で並んでいます.

孤立手話単語のデータセット (第2.2項-追跡点データをご参照ください) からフォーマットが変更されています.
統一したフォーマットに変換する方法は第4節で紹介します.

可視化例

図2に,データセットに含まれる指文字フレーズ の可視化例を示します.
図2(a)は,ロードした追跡点の可視化例です.
"536 verlin avenue" というフレーズを表出しています (最後にフレーズとは関係ない動作が含まれています).
見やすくするために一部の追跡点だけ描画しています.

また,本データセットには元動画が含まれていないので,参考までに高山の指文字動画を図2(b)に示します.
(見様見真似で行っているだけですので,注意してください.avenuevu になってしまっていますね...)

(a): 実サンプル
(b): 動画例
指文字フレーズデータの例

追加データ

supplemental_metadata.csvsupplemental_landmarks には利用可能な追加データが含まれています.
これらはフォーマットは同じですが,データ構成 (フレーズの内容など) に偏りがあります.

3. 統計情報

本節では,データセット全体の統計情報を解析したいと思います.

3.1 欠損データ

図2(a)から分かるように,追跡点データには追跡に失敗したフレームや部位 (以降,欠損データ) が含まれています.
ここでは,欠損データが含まれるサンプルと,どの部位の欠損データが多いかを解析してみます.

図3はデータセット内に欠損データが含まれるサンプルがどれだけあるかを示しています.
縦軸は全サンプルに対する欠損が含まれるサンプルの割合を示し,横軸は欠損が含まれる部位を示します.

図3: 欠損データが含まれるサンプル
欠損データが含まれるサンプル

Any parts に示されるように,ほとんどのサンプルで何かしらの欠損データが含まれていることが分かります.

All parts はフレーム全体が欠損していることを示しています.
今回のデータセットでは欠損フレームは見つかりませんでした.

他の縦棒は各部位の欠損を示しており,特に手の追跡が失敗するケースが多いことが分かります.
また,手が顔に重なるケースなどで,顔の追跡に失敗するケースもあるようです.

上記から分かるとおり,本データセットには欠損データが多数含まれているため,認識モデルには欠損データに対する各種の対処法を実装する必要があります.

3.2 入力系列長

次に,サンプルの入力系列長を解析してみます.

図4はデータセットの系列長分布を示しています.
縦軸は頻度を示しており,横軸は系列長のデータ区間 (ビンと言います) を示しています.

図4: 入力系列長の分布
入力系列長の分布

100から200フレーム程度 (3-7 秒程度) で動作が完了するサンプルが最も多くなっています.

一方,10 フレーム以下のサンプルや,800 フレーム (27秒程度) かかっているサンプルも含まれています (見えづらいですが,数サンプル程度あります).
このような極端な長さのサンプルは,撮影に失敗しているデータである可能性が高いです.
また,長いサンプルに合わせて学習を行うと,メモリを多く消費したり学習時間が長くなってしまったりします.

メモリ容量や処理速度が問題になる場合は,前処理でデータをカットしたり,系列長を変えたりなどの工夫をする必要が出てきます.

3.3 出力フレーズ長の分布

次に,サンプルの出力フレーズ長を解析してみます.

図5はデータセットのフレーズ長分布を示しています.
縦軸は頻度を示しており,横軸はフレーズに含まれる文字数を示しています.

図5: フレーズ長の分布
フレーズ長の分布

13文字のフレーズが最も多く,最長は31文字のフレーズです.
10文字以下のフレーズも小数ながら含まれています.

3.4 文字の出現頻度

最後に,各指文字の出現頻度を解析します.

図6は指文字の出現頻度分布を示しています.
縦軸は頻度を示しており,横軸は指文字を示しています.

図6: 文字の出現頻度
文字の出現頻度

数値,アルファベット,および特定の記号の出現頻度が多く,かなりバラツキがあることが分かります.
孤立手話単語の場合と異なり,連続指文字認識や連続手話単語認識,および手話翻訳では出力クラスの頻度を均一にすることは難しいです.
コンペティションでは,高頻度のクラスに注力することで好成績を狙うことも戦略上有りえますが,実製品の開発では対処法を検討する必要があります.

4. 実験用データセットの作成方法

ここから先は,実験用データセットの作成方法を説明します.
本記事で説明した各種の統計量の算出コードも記載しています.

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

まずは,前準備として Google Colab にテスト用のデータセットをアップロードします.

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

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

1
2
3
from google.colab import drive

drive.mount("/content/drive")

ドライブ内のファイルを Colab へコピーします.
パスはアップロード先を設定する必要があります.

# Copy to local.
!cp drive/MyDrive/Datasets/gafs_dataset_orig.zip gafs_orig.zip

データセットは ZIP形式になっているので unzip コマンドで解凍します.

!unzip -o gafs_orig.zip
Archive:  gafs_orig.zip
   creating: inputs/
   creating: inputs/google_fs/
   ...
  inflating: inputs/google_fs/train_landmarks/450474571.parquet

成功すると inputs/google_fs 以下にデータが解凍されます.

!ls inputs/google_fs
character_to_prediction_index.json  supplemental_metadata.csv  train_landmarks
supplemental_landmarks train.csv

データの内容については,第2.2項で説明したとおりです.

!cat inputs/google_fs/character_to_prediction_index.json
{
    " ":0,
    "!":1,
    ...
    "~":58
}
!cat inputs/google_fs/train.csv | head
path,file_id,sequence_id,participant_id,phrase
train_landmarks/5414471.parquet,5414471,1816796431,217,3 creekhouse
...

冒頭で説明したとおり,説明用のデータですので大部分の追跡点は含まれていません.

!ls inputs/google_fs/train_landmarks
450474571.parquet

4.2 データのロードとフォーマットチェック

まずはデータをロードして,フォーマットをチェックしていきます.
基本的には第3節で説明した内容の実行コードになります.

モジュールのインポート

下記のコードでモジュールをインポートします.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Standard modules.
import os
import json
import shutil
import time

# 3rd party's modules.
import numpy as np
import h5py
import pandas as pd
import matplotlib.pyplot as plt
【コード解説】
- 標準モジュール
  - os: OS依存の機能を操作するライブラリ.ファイルの読み書きをするために使用します.
  - json: JSONファイル制御ライブラリ.辞書ファイルのロードに使用します.
  - shutil: 高水準のファイル操作ライブラリ.
  - time: 時刻データの操作ライブラリ.処理時間を計測するために使用します.
- 3rdパーティモジュール
  - numpy: 行列演算ライブラリ.
  - h5py: HDF5ファイル操作ライブラリ.データベースの読み書きに使用します.
  - pandas: 表形式データ操作ライブラリ.
  - matplotlib: グラフ描画ライブラリ.

データセットのロードと基本情報の確認

下記のコードでデータセットをロードします.
データは pandas.DataFrame 型になっており,print() で表示すると第2.2項で説明した内容 (サンプル情報の段落をご参照ください) が表示されます.

1
2
3
4
# Load data definition.
root_dir = "inputs/google_fs"
track_info = pd.read_csv(os.path.join(root_dir, "train.csv"))
print(track_info)
                                     path     file_id  sequence_id  participant_id                          phrase
0         train_landmarks/5414471.parquet     5414471   1816796431             217                    3 creekhouse
1         train_landmarks/5414471.parquet     5414471   1816825349             107                 scales/kuhaylah
2         train_landmarks/5414471.parquet     5414471   1816909464               1             1383 william lanier
3         train_landmarks/5414471.parquet     5414471   1816967051              63               988 franklin lane
4         train_landmarks/5414471.parquet     5414471   1817123330              89       6920 northeast 661st road
...                                   ...         ...          ...             ...                             ...
67203  train_landmarks/2118949241.parquet  2118949241    388192924              88                    431-366-2913
67204  train_landmarks/2118949241.parquet  2118949241    388225542             154                    994-392-3850
67205  train_landmarks/2118949241.parquet  2118949241    388232076              95  https://www.tianjiagenomes.com
67206  train_landmarks/2118949241.parquet  2118949241    388235284              36               90 kerwood circle
67207  train_landmarks/2118949241.parquet  2118949241    388332538             176                      802 co 66b

下記に示すとおり,94 人分のデータが含まれています.

1
2
3
4
pids = track_info["participant_id"]
unique_pids = np.unique(pids)
print(unique_pids)
print(len(unique_pids))
[  0   1   2   4   6   9  10  13  15  18  20  21  24  25  27  33  36  38
  40  43  53  56  59  63  68  70  71  72  73  74  76  80  81  88  89  92
  93  95 102 105 107 109 112 113 115 117 121 122 125 128 135 136 138 141
 143 145 147 151 153 154 157 158 159 160 161 168 169 171 176 178 181 186
 187 188 192 196 202 203 216 217 219 223 225 227 230 231 233 236 239 241
 242 246 251 254]
94

下記に示すとおり,データ数は \(67208\) 個で 約\(70\%\) のデータは固有の指文字フレーズになっています.

1
2
3
4
5
6
7
8
phrases = track_info["phrase"]
unique_phrases = np.unique(phrases)
len_phrases = len(phrases)
len_unique_phrases = len(unique_phrases)
print(f"The number of total phrases:{len_phrases}")
print(f"Ther number of unique phrases:{len_unique_phrases}")
print(f"Ratio of unique phrases:{len_unique_phrases / len_phrases}")
print(f"Ratio of duplicate phrases:{(len_phrases - len_unique_phrases) / len_phrases}")
The number of total phrases:67208
Ther number of unique phrases:46478
Ratio of unique phrases:0.6915545768360909
Ratio of duplicate phrases:0.30844542316390905

出力フレーズ長分布の描画

第3.3項で説明した,出力フレーズ長の分布は下記の処理で描画しています.
まず,次のコードでフレーズ長を収集します.

1
2
3
lengths = np.array([len(p) for p in phrases])
max_len = np.max(lengths)
print(max_len)
31

次のコードで,フレーズ長のヒストグラムを描画します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
lengths = np.array([len(p) for p in phrases])
max_len = np.max(lengths)
histo, borders = np.histogram(lengths, bins=np.arange(1, max_len+1))

print(borders)
print(histo)

plt.figure(figsize=(14, 10))
plt.bar(borders[:-1]+1, histo, align="center")
plt.grid(axis="y", linestyle="--")
plt.xticks(fontsize=15)
plt.yticks(fontsize=15)
plt.xlabel("Phrase length", fontsize=20)
plt.ylabel("Frequency", fontsize=20)
plt.show()
[ 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]
[    1     0     5    27    66   136   225   421   815  1295  2634 11460
  3716  3760  3977  3563  3558  3423  3184  3171  3149  3115  2579  2212
  2061  1878  1966  1540  1767  1504]

ヒストグラムの棒を,区間の左側境界,右側境界のどちらに合わせて描画するかは毎回悩みます.
今回は区間が 1 刻みなので右側境界に合わせて描画しています.
(plt.bar(borders[:-1]+1) として,30 <= x <= 31 が31 の箇所に表示されるように処理しています).

文字の出現頻度の描画

第3.4項で説明した,出力フレーズ長の分布は下記の処理で描画しています.
まず,次のコードで辞書を読み込み,各文字の出現頻度を数えています.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
dict_file = os.path.join(root_dir, "character_to_prediction_index.json")
with open(dict_file, "r") as fread:
    key2token = json.load(fread)
characters = list(key2token.keys())
print(characters)
char_counter = {k: 0 for k in characters}

for phrase in phrases:
    for char in phrase:
        char_counter[char] += 1
print(char_counter)
[' ', '!', ... '~']
{' ': 58569, '!': 3, ... '~': 21}

次のコードで,文字出現頻度のヒストグラムを描画します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
x = np.arange(1, len(characters)+1)
y = char_counter.values()
label_x = char_counter.keys()

plt.figure(figsize=(14, 10))
plt.bar(x, y, align="center")
plt.xticks(x, label_x)
plt.grid(axis="y", linestyle="--")
plt.xticks(fontsize=15)
plt.yticks(fontsize=15)
plt.xlabel("Character", fontsize=20)
plt.ylabel("Frequency", fontsize=20)
plt.show()

x軸の目盛りは,plt.xticks(x, label_x) で置き換えています.

追跡点データのロードと確認

第2.2項で説明した (追跡点データの段落をご参照ください),追跡点データのフォーマットは下記のコードで確認します.
処理の都合で,統計情報は実験用データセットを作成した後に確認します.

次のコードで追跡点データをロードします.
データは pandas.DataFrame 型になっていますので,print() でフォーマットを確認することができます.

1
2
3
data_path  = os.path.join(root_dir, "train_landmarks/450474571.parquet")
data = pd.read_parquet(data_path)
print(data)
             frame  x_face_0  x_face_1  ...  z_right_hand_19  z_right_hand_20
sequence_id
2138557488       0  0.663001  0.644330  ...        -0.117422        -0.111925
2138557488       1  0.667972  0.643810  ...        -0.114211        -0.101464
2138557488       2  0.671009  0.642717  ...              NaN              NaN
2138557488       3  0.668623  0.641970  ...              NaN              NaN
2138557488       4  0.669397  0.647073  ...              NaN              NaN
...            ...       ...       ...  ...              ...              ...
2147465106     231  0.652571  0.639059  ...        -0.119032        -0.097088
2147465106     232  0.659162  0.641239  ...        -0.023387        -0.012794
2147465106     233  0.663675  0.649184  ...        -0.070979        -0.060423
2147465106     234  0.662469  0.648533  ...        -0.096455        -0.080358
2147465106     235  0.660208  0.641396  ...        -0.133947        -0.112528

下記に示すとおり,1 個 ファイルに複数のデータが含まれていることが分かります.

1
2
3
4
print(data.columns)
sids = data.index
unique_sids = np.unique(sids)
print(len(unique_sids))
Index(['frame', 'x_face_0', 'x_face_1', 'x_face_2', 'x_face_3', 'x_face_4',
       'x_face_5', 'x_face_6', 'x_face_7', 'x_face_8',
       ...
       'z_right_hand_11', 'z_right_hand_12', 'z_right_hand_13',
       'z_right_hand_14', 'z_right_hand_15', 'z_right_hand_16',
       'z_right_hand_17', 'z_right_hand_18', 'z_right_hand_19',
       'z_right_hand_20'],
      dtype='object', length=1630)
287

4.2 実験用データへの変換

本項では HDF5 形式の実験用データに変換する処理を説明します.

追跡点のロード処理

まず最初に,追跡点のロード処理を実装します.
Parquet 形式のデータは,カラム名を指定することで指定データだけを任意のカラム順で読み込むことができます.
ここでは孤立手話単語認識で用いたフォーマットと互換性を保ちたいので,下記のコードでカラム名を定義します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Define column names.
FACE_NUM = 468
LHAND_NUM = 21
POSE_NUM = 33
RHAND_NUM = 21
ROWS_PER_FRAME = FACE_NUM + LHAND_NUM + POSE_NUM + RHAND_NUM

FACE_COLUMNS = [[f"x_face_{i}", f"y_face_{i}", f"z_face_{i}"] for i in range(FACE_NUM)]
LHAND_COLUMNS = [[f"x_left_hand_{i}", f"y_left_hand_{i}", f"z_left_hand_{i}"] for i in range(LHAND_NUM)]
POSE_COLUMNS = [[f"x_pose_{i}", f"y_pose_{i}", f"z_pose_{i}"] for i in range(POSE_NUM)]
RHAND_COLUMNS = [[f"x_right_hand_{i}", f"y_right_hand_{i}", f"z_right_hand_{i}"] for i in range(RHAND_NUM)]
# Flatten.
FACE_COLUMNS = [item for sublist in FACE_COLUMNS for item in sublist]
LHAND_COLUMNS = [item for sublist in LHAND_COLUMNS for item in sublist]
POSE_COLUMNS = [item for sublist in POSE_COLUMNS for item in sublist]
RHAND_COLUMNS = [item for sublist in RHAND_COLUMNS for item in sublist]
# Concatenate.
COLUMNS = FACE_COLUMNS + LHAND_COLUMNS + POSE_COLUMNS + RHAND_COLUMNS

print(COLUMNS)
['x_face_0', 'y_face_0', 'z_face_0', ... 'x_right_hand_20', 'y_right_hand_20', 'z_right_hand_20']]

追跡点ごとに \((x, y, z)\) 座標がまとまるように順番を入れ替えています.
また,frame カラムは使用しないので削除しています.

次のコードで追跡点のロード関数を実装します.

1
2
3
4
5
def load_relevant_data_subset(pq_path):
    data = pd.read_parquet(pq_path, columns=COLUMNS)
    ids = data.index.unique()
    retval = {_id: np.array(data[data.index == _id], dtype=np.float32) for _id in ids}
    return retval

pd.read_parquet()columns 引数に先ほど定義したカラム名を渡している点に注意してください.
3-4行目の処理は sequence_id と追跡点データを紐づけています.

変換メイン処理

次のコードで変換のメイン処理を実装します.

 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
outdir = "dataset_fs"
shutil.rmtree(outdir, ignore_errors=True)
os.makedirs(outdir, exist_ok=True)

paths = np.unique(np.array(track_info["path"]))
for path in paths:
    pq_path = os.path.join(root_dir, path)
    if not os.path.exists(pq_path):
        continue

    start = time.perf_counter()
    data = load_relevant_data_subset(pq_path)
    sids = list(data.keys())
    for sid in sids:
        meta = track_info[track_info["sequence_id"] == sid]
        pid = meta["participant_id"].values[0]
        phrase = meta["phrase"].values
        if isinstance(phrase, (tuple, list, np.ndarray)):
            phrase = phrase[0]
        tokens = np.array([key2token[p] for p in phrase])
        track = data[sid]
        # `[T, J, C] -> [C, J, T]`
        track = track.reshape([track.shape[0], ROWS_PER_FRAME, -1])
        track = np.transpose(track, [2, 0, 1])
        outpath = os.path.join(outdir, str(pid) + ".hdf5")
        mode = "a" if os.path.exists(outpath) else "w"
        with h5py.File(outpath, mode) as fwrite:
            # Create group and write data.
            grp = fwrite.create_group(str(sid))
            grp.create_dataset("feature", data=track)
            grp.create_dataset("token", data=tokens)
    interval = time.perf_counter() - start
    print(f"Processed {path} in {interval} seconds.")
【コード解説】
- 1-3行目: 出力先ディレクトリ作成
- 5行目: 追跡点ファイル一覧を取得
- 6-32行目: 追跡点ファイル毎に変換処理
  - 12行目: 追跡点データ読み込み
  - 14-30行目: 追跡点データ毎に変換
    - 15-20行目: 関連情報をロードしてフレーズを数値列に変換
    - 21-23行目: 追跡点データを成形
    - 24-26行目: 出力ファイルオープン.ファイルが存在する場合は追記モードで開きます.
    - 28-30行目: データ書き込み
  - 31-32行目: 処理時間出力
Processed train_landmarks/450474571.parquet in 4.616932380999998 seconds.

次のコードで辞書ファイルをコピーします.

# Copy dictionary.
shutil.copy(dict_file, os.path.join(outdir, "character_to_prediction_index.json"))
dataset_fs/character_to_prediction_index.json

次のコマンドで結果を確認します.

!ls -alhs dataset_fs
total 289M
4.0K drwxr-xr-x 2 root root 4.0K Sep  7 14:49 .
4.0K drwxr-xr-x 1 root root 4.0K Sep  7 14:49 ..
776K -rw-r--r-- 1 root root 775K Sep  7 14:49 0.hdf5
...
4.0K -rw-rw-rw- 1 root root  405 Sep  7 14:49 character_to_prediction_index.json

ここでは 1 ファイルだけなので容量は少ないですが,全データを処理すると66G バイト程度になります.

4.3 追跡点の統計情報の解析

順番が前後してしまいましたが,第3.1項第3.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
45
46
47
48
49
50
51
52
53
54
55
files = os.listdir(outdir)
files = [fin for fin in files if ".hdf5" in fin]
tlen_summary = {}
count_nan = {
    "total": 0,
    "any": 0,
    "all": 0,
    "face": 0,
    "lhand": 0,
    "pose": 0,
    "rhand": 0,
    "bhand": 0}

for fin in files:
    name = fin.split(".")[0]
    path = os.path.join(outdir, fin)
    with h5py.File(path, "r") as fread:
        keys = list(fread.keys())
        for key in keys:
            # Count body-parts including nan.
            group = fread[key]
            track = group["feature"][:]
            # `[C, T, J] -> [T, J, C]`
            track = np.transpose(track, [1, 2, 0])

            face = track[:, :468]
            lhand = track[:, 468: 468+21]
            pose = track[:, 468+21: 468+21+33]
            rhand = track[:, 468+21+33:]
            count_nan["total"] += 1
            if np.isnan(track).any():
                count_nan["any"] += 1
            if np.isnan(face).any():
                count_nan["face"] += 1
            if np.isnan(lhand).any():
                count_nan["lhand"] += 1
            if np.isnan(pose).any():
                count_nan["pose"] += 1
            if np.isnan(rhand).any():
                count_nan["rhand"] += 1
            if np.isnan(lhand).any() and np.isnan(rhand).any():
                count_nan["bhand"] += 1

            # Check drop.
            track = track.reshape([track.shape[0], -1])
            mask = np.isnan(track)
            drops = np.all(mask, axis=1).sum()
            if drops > 0:
                count_nan["all"] += 1

            # Store temporal length.
            if name not in tlen_summary:
                tlen_summary[name] = [track.shape[0]]
            else:
                tlen_summary[name].append(track.shape[0])

基本的には,地道にループを回して数えているだけです.

次のコードで部位ごとの欠損データの割合を描画しています.

 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
# Plot character frequency distribution.
print(count_nan)
total_c = count_nan["total"]
any_c = count_nan["any"]
all_c = count_nan["all"]
face_c = count_nan["face"]
lhand_c = count_nan["lhand"]
pose_c = count_nan["pose"]
rhand_c = count_nan["rhand"]
bhand_c = count_nan["bhand"]

any_r = any_c / total_c
all_r = all_c / total_c
face_r = face_c / total_c
lhand_r = lhand_c / total_c
pose_r = pose_c / total_c
rhand_r = rhand_c / total_c
bhand_r = bhand_c / total_c

ys = [any_r, all_r, face_r, lhand_r, rhand_r, bhand_r, pose_r]
xticks = ["Any parts", "All parts", "Face", "Lhand", "Rhand", "Both hands", "Pose"]
xs = np.arange(len(ys))
print(ys)

plt.figure(figsize=(14, 10))
plt.bar(xs, ys, align="center")
plt.xticks(xs, xticks)
plt.grid(axis="y", linestyle="--")
plt.xticks(fontsize=15)
plt.yticks(fontsize=15)
plt.ylim([0.0, 1.0])
plt.xlabel("Body-parts", fontsize=20)
plt.ylabel("Ratio", fontsize=20)
plt.show()
{'total': 287, 'any': 287, 'all': 0, 'face': 27, 'lhand': 286, 'pose': 0, 'rhand': 270, 'bhand': 269}
[1.0, 0.0, 0.09407665505226481, 0.9965156794425087, 0.9407665505226481, 0.9372822299651568, 0.0]

次のコードで入力系列長分布を描画します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Plot temporal length's distribution.
dist = []
for values in tlen_summary.values():
    dist += values
dist = np.array(dist)
histo, bins = np.histogram(dist, bins=np.arange(10, dist.max()+10, 10))
print(histo, len(histo))
print(bins, len(bins))

plt.figure(figsize=(14, 10))
plt.bar(bins[:-1], histo, width=3, align="center")
plt.grid(axis="y", linestyle="--")
plt.xticks(fontsize=15)
plt.yticks(fontsize=15)
plt.xlabel("Frame", fontsize=20)
plt.ylabel("Frequency", fontsize=20)
plt.show()

ここではヒストグラム区間の左側境界上に棒が立つように描画しています.
(plt.bar(borders[:-1]) として,0 <= x < 10 が 0 の箇所に表示されるように処理しています).


今回は Kaggle の指文字認識コンペティションで用いられた GAFS データセットについて紹介しましたが,如何でしたでしょうか?

これだけのサイズの連続指文字認識用データが商用可能ライセンスで出たのはインパクトがでかいですね.
ただ,指文字メインだと実用的なアプリケーションを企画する方が大変かもしれませんが...

また,このデータセットは指文字認識が対象ですが,考え方やフォーマットは連続手話単語認識や手話翻訳に応用可能です.
商用利用可能な連続手話単語認識や手話翻訳データセットは (高山の知る限り) 未だありませんが,研究開発の足がかりとしては十分利用できるのではないでしょうか.

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