手話認識入門 補足 - 実験用GISLRデータセットの作成方法

This image is generated with ChatGPT-4 Omni, and edited by the author.
作成日:2024年07月24日(水) 00:00
最終更新日:2024年09月18日(水) 09:52
カテゴリ:手話言語処理
タグ:  手話認識 孤立手話単語認識 GISLR データセット Python

手話認識入門で使用している,HDF5形式のGISLRデータセット作成方法を紹介します.

こんにちは.高山です.
本連載記事では実験で,KaggleのGoogle Isolated Sign Language Recognition (以下,GISLR) で用いられたデータセットをHDF5に加工して使っています.
本当に今更で恐縮なのですが,GISLRデータセットをHDF5形式にまとめる方法を全く説明していなかったことに気づきました(^^;).

そこで今回は,GISLRデータセットをHDF5形式に変換する方法を紹介したいと思います.

今回解説するスクリプトはGitHub上に公開しています
Colab上で使用しているデータセットは,説明用に大部分の追跡点を削除したデータセットです.
ご自分の環境で試す場合は,オリジナルのデータセットをローカル環境で (Google Driveに入り切らないため) 使用するようにしてください.

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

  • 2024/09/18: カテゴリを変更しました
  • 2024/09/17: タグとサマリを更新しました

1. GISLRデータセットのダウンロード

オリジナルのデータセットの入手方法については,こちらの記事で解説しています (第2.1項をご参照ください).

ダウンロードが完了すると,asl-signs.zip というZIPファイルが入手でき,中身は下記のようになっています.

$ unzip asl-signs.zip
$ ls asl-signs
sign_to_prediction_index_map.json  train.csv  train_landmark_files

今回は,これらのファイルがローカル環境または Colab からアクセス可能な場所に配置してある,という前提のもと説明していきます.

2. 前準備

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

ここからはColab上のスクリプトの内容を説明していきます.
まずは,前準備として 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/gislr_dataset_orig.zip gislr_orig.zip

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

!unzip -o gislr_orig.zip
Archive:  gislr_orig.zip
   creating: inputs/google_islr/
  inflating: inputs/google_islr/sign_to_prediction_index_map.json
  ...
  inflating: inputs/google_islr/train_landmark_files/62590/1000240708.parquet

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

!ls inputs/google_islr
sign_to_prediction_index_map.json  train.csv  train_landmark_files

2.2 データセットの内容

辞書ファイル

単語辞書には単語名と数値の関係が250単語分定義されています.

!cat inputs/google_islr/sign_to_prediction_index_map.json
{"TV": 0, "after": 1, ..., "zipper": 249}

サンプル情報

train.csv は各サンプルの情報を表形式でまとめたデータです.

!cat inputs/google_islr/train.csv | head
path,participant_id,sequence_id,sign
train_landmark_files/26734/1000035562.parquet,26734,1000035562,blow
train_landmark_files/28656/1000106739.parquet,28656,1000106739,wait
train_landmark_files/16069/100015657.parquet,16069,100015657,cloud
train_landmark_files/25571/1000210073.parquet,25571,1000210073,bird
train_landmark_files/62590/1000240708.parquet,62590,1000240708,owie
train_landmark_files/26734/1000241583.parquet,26734,1000241583,duck
train_landmark_files/26734/1000255522.parquet,26734,1000255522,minemy
train_landmark_files/32319/1000278229.parquet,32319,1000278229,lips
train_landmark_files/37055/100035691.parquet,37055,100035691,flower

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

  • path: 骨格追跡点データへのパス
  • participant_id: 手話話者ID
  • sequence_id: データ番号
  • sign: 何の手話単語を表出しているか

追跡点データ

train_landmarks は追跡点データが格納されているディレクトリです.
中身は話者ID毎のサブディレクトリで構造化されています.

!ls inputs/google_islr/train_landmark_files
16069  2044   25571  27610  29302  32319  36257  37779  49445  55372  62590
18796  22343  26734  28656  30680  34503  37055  4718   53618  61333

テスト用データセットには,各ディレクトリに1ファイルだけ追跡点データが含まれています.

!ls inputs/google_islr/train_landmark_files/16069
100015657.parquet

今回はこのテスト用データセットを用いてHDF5形式への変換処理を組んでいきます.
本番ではオリジナルのデータセットに差し替えてご利用いただければと思います.

3. HDF5形式への変換処理

3.1 モジュールのロード

主要な処理の実装に先立って,下記のコードでモジュールをロードします.

1
2
3
4
5
6
7
8
# Standard modules.
import os
import json

# 3rd party's modules.
import numpy as np
import h5py
import pandas as pd
【コード解説】
- 標準モジュール
  - json: JSONファイル制御ライブラリ.辞書ファイルのロードに使用します.
  - os: システム処理ライブラリ
- 3rdパーティモジュール
  - numpy: 行列演算ライブラリ
  - h5py: HDF5ファイル処理ライブラリ
  - pandas: データ解析支援ライブラリ.
    今回は parquet 形式のファイルをロードするために用います.

3.2 追跡点の読み込み処理

まずは追跡点の読み込み処理を実装します.
追跡点のデータフォーマットは下記のようになっています.
(こちらの記事でも解説しています (第2.2項-追跡点データをご参照ください).)

       frame            row_id        type  landmark_index         x         y         z
0         20         20-face-0        face               0  0.494400  0.380470 -0.030626
1         20         20-face-1        face               1  0.496017  0.350735 -0.057565
2         20         20-face-2        face               2  0.500818  0.359343 -0.030283
3         20         20-face-3        face               3  0.489788  0.321780 -0.040622
4         20         20-face-4        face               4  0.495304  0.341821 -0.061152
...      ...               ...         ...             ...       ...       ...       ...
12484     42  42-right_hand-16  right_hand              16  0.001660  0.549574 -0.145409
12485     42  42-right_hand-17  right_hand              17  0.042694  0.693116 -0.085307
12486     42  42-right_hand-18  right_hand              18  0.006723  0.665044 -0.114017
12487     42  42-right_hand-19  right_hand              19 -0.014755  0.643799 -0.123488
12488     42  42-right_hand-20  right_hand              20 -0.031811  0.627077 -0.129067

[12489 rows x 7 columns]

データは表形式で格納されており,各カラムの内容は次のとおりです.

  • frame: 追跡点系列のフレーム番号.上の例で示されるとおり,0始まりではありません.
    また,途中抜けの番号もありますが評価で用いられるコードを見る限りでは,そのようなフレームは単純に無視されるようです.
  • row_id: 各行のユニークID.[frame][type][landmark_index] の形式になっています.
  • type: 身体部位名.face, left_hand, pose, right_hand のどれかになります.
  • landmark_index: 各部位の追跡点番号
  • x, y, z: 追跡点の座標値

この並び方は全データに共通しているため,各カラムを細かく追いかける必要はなく,下記に示す処理で追跡点を統一的にロードすることができます.

1
2
3
4
5
6
7
8
ROWS_PER_FRAME = 543  # Number of landmarks per frame.

def load_relevant_data_subset(pq_path):
    data_columns = ['x', 'y', 'z']
    data = pd.read_parquet(pq_path, columns=data_columns)
    n_frames = int(len(data) / ROWS_PER_FRAME)
    data = data.values.reshape(n_frames, ROWS_PER_FRAME, len(data_columns))
    return data.astype(np.float32)
【コード解説】
- 1行目: 追跡点の個数を事前定義
- 4-5行目: `x`, `y`, `z` のカラムだけロード
- 6-7行目: `[T, J, C]` の形状に変形して返す
  - T: フレーム数
  - J: 追跡点数.ここでは543
  - C: 特徴量数.ここでは3

3.3 変換処理

ここから先は変換処理本体を実装していきます.
まず次のコードで,サンプル情報を読み込みます.

1
2
3
4
# Load data definition.
root_dir = "inputs/google_islr"
track_info = pd.read_csv(os.path.join(root_dir, "train.csv"))
print(track_info)
# Load data definition.
root_dir = "inputs/google_islr"
track_info = pd.read_csv(os.path.join(root_dir, "train.csv"))
print(track_info)
                                                path  participant_id sequence_id    sign
0      train_landmark_files/26734/1000035562.parquet           26734  1000035562    blow
1      train_landmark_files/28656/1000106739.parquet           28656  1000106739    wait
2       train_landmark_files/16069/100015657.parquet           16069   100015657   cloud
3      train_landmark_files/25571/1000210073.parquet           25571  1000210073    bird
4      train_landmark_files/62590/1000240708.parquet           62590  1000240708    owie
...                                              ...             ...         ...     ...
94472   train_landmark_files/53618/999786174.parquet           53618   999786174   white
94473   train_landmark_files/26734/999799849.parquet           26734   999799849    have
94474   train_landmark_files/25571/999833418.parquet           25571   999833418  flower
94475   train_landmark_files/29302/999895257.parquet           29302   999895257    room
94476   train_landmark_files/36257/999962374.parquet           36257   999962374   happy

[94477 rows x 4 columns]

次に,下記のコードで手話者IDを読み込みます.

1
2
3
4
# Extract unique participant ids.
pids = np.array(track_info["participant_id"])
upids = np.unique(pids)
print(upids)
[ 2044  4718 16069 18796 22343 25571 26734 27610 28656 29302 30680 32319
 34503 36257 37055 37779 49445 53618 55372 61333 62590]

下記のコードで辞書データを読み込みます.

1
2
3
4
5
# Load sign dictionary.
dictfile = os.path.join(root_dir, "sign_to_prediction_index_map.json")
with open(dictfile, "r") as fread:
    dictionary = json.load(fread)
print(dictionary)
{'TV': 0, 'after': 1, ..., 'zipper': 249}

変換の準備ができましたので,下記のコードで変換処理を実装します.

 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
# Conversion.
convert_to_channel_first = True
outdir = "dataset"
os.makedirs(outdir, exist_ok=True)

for upid in upids:
    temp_info = track_info[track_info["participant_id"] == upid]
    outpath = os.path.join(outdir, str(upid) + ".hdf5")
    with h5py.File(outpath, "w") as fwrite:
        for info in temp_info.itertuples(index=False):
            path = info[0]
            pid = info[1]
            sid = info[2]
            token = np.array([dictionary[info[3]]])
            assert pid == upid, f"{pid}:{upid}"
            track_path = os.path.join(root_dir, path)
            if not os.path.exists(track_path):
                continue
            track = load_relevant_data_subset(track_path)
            # `[T, J, C] -> [C, T, J]`
            if convert_to_channel_first:
                track = track.transpose([2, 0, 1])

            # Create group.
            grp = fwrite.create_group(str(sid))
            grp.create_dataset("feature", data=track)
            grp.create_dataset("token", data=token)
【コード解説】
- 2行目: `[C, T, J]` 形式で保存するように指定
- 3-4行目: 出力ディレクトリを作成
- 6-27行目: 変換処理ループ.
  手話者毎にHDF5ファイルを作成して,データを格納します.
  - 7行目: サンプル情報を手話者IDでフィルタリング
  - 8行目: 出力ファイル名を定義
  - 9行目: HDF5ファイルオープン
  - 10-27行目: 変換処理
    `itertuples()` を用いてサンプル情報を 1 行ずつ取り出しています.
    - 11-15行目: サンプル情報を取り出して,変数に格納
    - 16-19行目: 追跡点データ読み込み
    - 21-22行目: `[T, J, C] -> [C, T, J]` に変換
    - 25-27行目: サンプルID毎にグループを作成して,データを格納.
      追跡点データは `feature`,単語ID は `token` というキー値で格納しています.

4. 出力ファイルの確認

では,データが正しく保存されているかを確認していきます.

!ls dataset
16069.hdf5  22343.hdf5  27610.hdf5  30680.hdf5  36257.hdf5  4718.hdf5   55372.hdf5
18796.hdf5  25571.hdf5  28656.hdf5  32319.hdf5  37055.hdf5  49445.hdf5  61333.hdf5
2044.hdf5   26734.hdf5  29302.hdf5  34503.hdf5  37779.hdf5  53618.hdf5  62590.hdf5

[話者ID].hdf5 という形式で保存されていることが分かります.

次のコードでHDF5ファイルの中身を確認します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Test loading.
with h5py.File("dataset/16069.hdf5", "r") as fread:
    keys = fread.keys()
    print("Groups:", keys)
    for key in keys:
        data = fread[key]
        print("Data in a group:", data.keys())
        feature = data["feature"][:]
        token = data["token"][:]
        print(feature.shape)
        print(token)
Groups: <KeysViewHDF5 ['100015657']>
Data in a group: <KeysViewHDF5 ['feature', 'token']>
(3, 105, 543)
[48]

HDF5ファイルは辞書データのように,キーバリュー形式でデータを取り出せます.
下記のような階層関係でデータが保存されていることが分かると思います.

- 100015657: サンプルID
  |- feature: 追跡点データ
  |- token: 単語ID

今回は,GISLRデータセットをHDF5形式に変換してまとめる方法を紹介しましたが,如何でしたでしょうか?

機械学習用データセットは個別のフォーマットを採用しており,毎回専用の IO 処理を実装するのは少し面倒です.
(可能な場合は) 慣れているフォーマットに変換すると後段の処理変更が少なく済むことがあるため,本連載ではHDF5形式に変換したデータセットを使用しています.

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