実践手話認識 - モデル開発編2: Conformerを用いた孤立手話単語認識モデル

Thumbnail image
This image is generated with ChatGPT-4 Omni, and edited by the author.
作成日: 2024年08月31日(土) 00:00
最終更新日: 2024年09月18日(水) 09:52
カテゴリ: 手話言語処理
タグ:  実践手話認識 孤立手話単語認識 Conformer Python

Conformerを用いた孤立手話単語認識モデルを紹介します.

こんにちは.高山です.
実践手話認識 - モデル開発編の第二回になります.

今回は Conformer [Gulati'20] を用いた孤立手話単語認識モデルを実装する方法を紹介します.
Conformer は,第一回で紹介した Macaron Net に Convolutional Neural Network (CNN) をベースとした処理ブロック (Convolution module) を追加したモデルです.
(実際は Positional encoding や活性化関数なども改良されています)

Transformer [Vaswani'17] の Multi-head self attention (MHSA) では,あるフレームの特徴抽出を行う際に全フレームの特徴を取り込みながら計算を行います.
一方 Conformer の Convolution module では,計算対象フレームと近接フレームの特徴を用いて特徴抽出を行います.

全体的な関係性と局所的な関係性 (文献[Gulati'20] では,それぞれ global context と local context と呼んでいます) のそれぞれに着目した特徴抽出を併用することで,相補的な効果を狙います.

Macaron Net [Lu'19] からスムーズに拡張できますので,今回はこちらの手法を実装して性能が向上するかどうか実験してみたいと思います.

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

  • [Gulati'20]: A. Gulati, et al., "Conformer: Convolution-augmented Transformer for Speech Recognition," Proc. of the Interspeech, available here, 2020.
  • [Vaswani'17]: A. Vaswani, et al., "Attention Is All You Need," Proc. of the NIPS, available here, 2017.
  • [Lu'19]: Y. Lu, et al., "Understanding and Improving Transformer From a Multi-Particle Dynamic System Point of View," arXiv:1906.02762, available here, 2019.

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

  • 2024/09/17: タグを更新しました

1. 実験概要

図1に今回の実験内容を示します.

図1: 今回の実験内容
今回の実験内容

図1 の LN, PFFN, MHSA は,それぞれ Layer normalization [Ba'16] と Position-wise feed forward network, および Multi-head self attention を示します.
第一回で紹介した Macaron Net をベースとして,Convolution module を追加して認識性能を比較します.

その他の変更した点,変更しなかった点は下記のとおりです.

  • 活性化関数: Conformer は ReLU [Fukushima'69] から Swish [Ramachandran'17] へ変更.
    (ReLU ってネオコグニトロン (CNN の原型) で有名な福島先生が最初だったのですね.不勉強で知りませんでした...)
  • 位置エンコーディング: 文献[Gulati'20] では 相対位置エンコーディング[Dai'19] を用いているが,Transformer の手法のままにする.

位置エンコーディングを変更しなかった理由は,単に実装が大変そうだったからです(^^;).
同時に変更すると話が混み合うという理由もありますが...

  • [Ba'16]: J. Ba, et al., "Layer Normalization," arXiV: 1607.06450, available here, 2016.
  • [Fukushima'69]: K. Fukushima, "Visual Feature Extraction by a Multilayered Network of Analog Threshold Elements," IEEE. Trans. Syst. Man. Cybern., Vol.5, Issue 4, pp.322-333, 1969.
  • [Ramachandran'17]: P. Ramachandran, et al., "Searching for Activation Functions," Proc of the ICLR Workshop, available here, 2018.
  • [Dai'19]: Z. Dai, et al., "Transformer-XL: Attentive Language Models beyond a Fixed-Length Context," Proc of the ACL, available here, 2019.

2. Convolution module

Convolution module のブロック図を図2に示します.
なお,Dropout 層は省略しています.

図2: Convolution module
Convolution module

基本的には,CNN \(\rightarrow\) 正規化層 \(\rightarrow\) 活性化関数,という画像処理系モデルでよく見る構造ですが,いくつかの点で異なります.

Depthwise Separable convolution の採用

Convolution module では,標準 CNN の代わりに活性化関数付きの Depthwise Separable convolution (DSCNN) [Chollet'17] を用います.

標準の CNN では,特徴次元の変更と時空間のフィルタ処理を同時に行います.
一方 DSCNN では,特徴次元の変更と (チャネル毎の) 時空間フィルタ処理を,それぞれ Pointwise convolution (PCNN) と Depthwise convolution (DCNN) を用いて順番に行います.
この設計により CNN の処理ブロックに必要な学習パラメータ数を減らすことができます.

また,PCNN と DCNN の間に Gated linear unit (GLU) 活性化関数 [Dauphin'17] を挟むことで,認識に不要な特徴を抑制しながら特徴抽出を行っています.

余談ですが,PCNN と DCNN はそれぞれ下記の処理と等価な処理になります.

  • PCNN: Linear 層
  • DCNN: Grouped convolution において,\(\text{[Group数]} = \text{[チャネル数]}\) とした場合

個人的には分かりにくいと感じており,別名を付ける必要はなかったのでは (特にDCNN) ,と思っています.

ブロックの末尾で再度 Point-wise convolution を適用

ブロック末尾の PCNN については文献[Gulati'20] でほぼ言及がありません.
ただし,ブロック末尾の PCNN を削除すると Batch Normalization \(\rightarrow\) (次のブロックの) Layer Normalziation と正規化処理が連続してしまいます.

そのため正規化処理が連続するのを防ぐ目的で入れているのでは,と思っています.

  • [Chollet'17]: F. Chollet'17, et al., "Xception: Deep Learning with Depthwise Separable Convolutions," Proc. of the CVPR, available here, 2017.
  • [Dauphin'17]: Y. N. Dauphin,17, et al., "Language modeling with gated convolutional networks," Proc. of the ICML, available here, 2017.

3. 実験結果

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

なお,今回の実験から学習を安定させるために,ラベルスムージングと各種のデータ拡張処理を導入しています.
これらの処理については「手話認識入門」の記事で解説しているので,よろしければご一読ください.

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

図3は,アーキテクチャ毎のValidation Lossと認識率の推移を示しています.

図3: 認識性能比較結果 (Top10)
認識性能比較結果 (Top10)

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

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

前処理の追加によって全体的に性能が上がっているのは良いですが...,正直あまり性能差は感じられませんね...(^^;)
10単語を用いた実験は性能が飽和し始めているのかもしれません.

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

全データ (250単語) を学習させた場合の挙動を図4に示します.
なお,こちらの実験はメモリや処理時間の都合でColab上では実行が難しいので,ローカル環境で行いました.

データの分割方法やパラメータは10単語のときと同じです.
ただし,学習エポック数は100,バッチ数は256に設定しています.

図4: 認識性能比較結果 (Full)
認識性能比較結果 (Full)

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

  • 青線 (Transformer): Pre-LN構成のTransformer
  • 橙線 (Macaron-Net): Macaron Net-Lと同条件
  • 緑線 (Conformer (K=3)): Conformer (カーネルサイズ 3)
  • 赤線 (Conformer (K=5)): Conformer (カーネルサイズ 5)
  • 紫線 (Conformer (K=7)): Conformer (カーネルサイズ 7)

Conformer に関してはカーネルサイズを変更した評価も行っています.

今回の実験では,カーネルサイズが 5 の Conformer が最も良い性能となりました.
ただし劇的に良くなるというわけではなく,設定次第では性能が悪くなる (例えば紫線) こともあるようです.

前処理を入れた効果だと思いますが,いずれのモデルも100 エポック時点で性能が収束していないようですね.
学習を続ければ認識率 60 % 弱くらいまでは性能を伸ばせるかもしれません.

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

4. 前準備

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

ここからは実装方法の説明をしていきます.
まずは,前準備としてGoogle Colabにデータセットをアップロードします. ここの工程はこれまでの記事と同じですので,既に行ったことのある方は第4.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]

4.2 モジュールのダウンロード

次に,過去の記事で実装したコードをダウンロードします.
本項は前回までに紹介した内容と同じですので,飛ばしていただいても構いません. コードはGithubのsrc/modules_gislrにアップしてあります (今後の記事で使用するコードも含まれています).

まず,下記のコマンドでレポジトリをダウンロードします.

!wget https://github.com/takayama-rado/trado_samples/archive/refs/tags/v0.2.1.zip -O master.zip
--2024-08-26 12:12:17--  https://github.com/takayama-rado/trado_samples/archive/refs/tags/v0.2.1.zip
...
2024-08-26 12:12:24 (12.5 MB/s) - ‘master.zip’ saved [80003316]

ダウンロードしたリポジトリを解凍します.

!unzip -o master.zip -d master
Archive:  master.zip
23fc135cb7417554dafb2eea052df0791ac3e1fd
   creating: master/trado_samples-0.2.1/
  inflating: master/trado_samples-0.2.1/.gitignore
  ...

モジュールのディレクトリをカレントディレクトリに移動します.

!mv master/trado_samples-0.2.1/src/modules_gislr .

他のファイルは不要なので削除します.

!rm -rf master master.zip gislr_top10.zip
!ls
dataset_top10 drive modules_gislr  sample_data

4.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
51
52
53
54
55
56
57
58
59
60
61
62
63
import copy
import json
import os
import sys
from functools import partial
from inspect import signature
from pathlib import Path

# 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,
    get_fullbody_swappairs
)
from modules_gislr.layers import (
    Identity,
    GPoolRecognitionHead,
    PositionalEncoding,
    MultiheadAttention,
    PositionwiseFeedForward,
    TransformerEncoderLayer,
    TransformerEncoder,
    TransformerEnISLR,
    MacaronNetEnISLR,
    apply_norm,
    create_norm
)
from modules_gislr.train_functions import (
    test_loop,
    val_loop,
    train_loop
)
from modules_gislr.transforms import (
    PartsBasedNormalization,
    RandomAffineTransform2D,
    RandomClip,
    RandomDropParts,
    RandomHorizontalFlip,
    RandomNoise,
    RandomTimeWarping,
    ReplaceNan,
    SelectLandmarksAndFeature,
    ToTensor
)
from modules_gislr.utils import (
    make_causal_mask,
    make_san_mask,
    select_reluwise_activation
)
【コード解説】
- 標準モジュール
  - copy: データコピーライブラリ.Macaron Netブロック内でEncoder層をコピーするために使用します.
  - json: JSONファイル制御ライブラリ.辞書ファイルのロードに使用します.
  - os: システム処理ライブラリ
  - sys: Pythonインタプリタの制御ライブラリ.
    今回はローカルモジュールに対してパスを通すために使用します.
  - functools: 関数オブジェクトを操作するためのライブラリ.
    今回はDataLoaderクラスに渡すパディング関数に対して設定値をセットするために使用します.
  - inspect.signature: オブジェクトの情報取得ライブラリ.
  - pathlib.Path: オブジェクト指向のファイルシステム機能.
    主にファイルアクセスに使います.osモジュールを使っても同様の処理は可能です.
    高山の好みでこちらのモジュールを使っています(^^;).
- 3rdパーティモジュール
  - numpy: 行列演算ライブラリ
  - torch: ニューラルネットワークライブラリ
  - torchvision: PyTorchと親和性が高い画像処理ライブラリ.
    今回はDatasetクラスに与える前処理をパッケージするために用います.
- ローカルモジュール: sys.pathにパスを追加することでロード可能
  - dataset: データセット操作用モジュール
  - defines: 各部位の追跡点,追跡点間の接続関係,およびそれらへのアクセス処理を
    定義したモジュール
  - layers: ニューラルネットワークのモデルやレイヤモジュール
  - transforms: 入出力変換処理モジュール
  - train_functions: 学習・評価処理モジュール
  - utils: 汎用処理関数モジュール

5. 認識モデルの実装

5.1 Convolution module

ここから先は,認識モデルを実装していきます.
まず最初に,下記のコードで Convolution module を実装します.

 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
class ConformerConvBlock(nn.Module):
    def __init__(self,
                 dim_model,
                 kernel_size,
                 norm_type="batch",
                 activation="swish",
                 padding_mode="zeros",
                 causal=False):
        super().__init__()

        assert norm_type in ["layer", "batch"]
        assert (kernel_size - 1) % 2 == 0, f"kernel_size:{kernel_size} must be the odd number."
        assert kernel_size >= 3, f"kernel_size: {kernel_size} must be larger than 3."
        self.kernel_size = kernel_size
        self.norm_type = norm_type
        self.causal = causal

        if causal:
            self.padding = (kernel_size - 1)
        else:
            self.padding = (kernel_size - 1) // 2

        self.pointwise_conv1 = nn.Conv1d(
            in_channels=dim_model,
            out_channels=dim_model * 2,  # for GLU
            kernel_size=1,
            stride=1,
            padding=0)
        self.depthwise_conv = nn.Conv1d(
            in_channels=dim_model,
            out_channels=dim_model,
            kernel_size=kernel_size,
            stride=1,
            padding=self.padding,
            groups=dim_model,  # Depthwise
            padding_mode=padding_mode,
            bias=True)

        self.norm = create_norm(norm_type, dim_model)

        self.activation = select_reluwise_activation(activation)

        self.pointwise_conv2 = nn.Conv1d(
            in_channels=dim_model,
            out_channels=dim_model,
            kernel_size=1,
            stride=1,
            padding=0)

    def forward(self,
                feature):
        # `[N, T, C] -> [N, C, T] -> [N, 2C, T] -> [N, C, T]`
        feature = feature.permute([0, 2, 1]).contiguous()
        feature = self.pointwise_conv1(feature)
        feature = F.glu(feature, dim=1)

        # `[N, C, T] -> [N, C, T]`
        feature = self.depthwise_conv(feature)
        if self.causal:
            feature = feature[:, :, :-self.padding]

        # `[N, C, T]`: channel_first
        feature = apply_norm(self.norm, feature, channel_first=True)

        feature = self.activation(feature)
        feature = self.pointwise_conv2(feature)

        # `[N, C, T] -> [N, T, C]`
        feature = feature.permute([0, 2, 1]).contiguous()
        return feature
【コード解説】
- 引数
  - dim_model: 入力特徴量の次元数
  - kernel_size: CNN層のカーネルサイズ.
    奇数を指定する必要があります.
  - norm_type: 正規化層の種別を指定 [batch/layer]
  - activation: 活性化関数の種別を指定 [relu/gelu/swish/silu/mish]
  - padding_mode: CNN層のパディング方法を指定 [zeros/reflect/replicate/circular]
  - causal: True の場合,Causal convolution を適用します.
    この手法では未来の情報を含まないように畳み込みを行います.
    リアルタイム処理など,現在のフレームよりも過去の情報しか得られない場合の
    利用を想定しています.
- 9-48行目: 初期化処理.
  - 18-21行目: Causal convolution の場合は畳み込み範囲が過去フレーム側にずれる
    ので,パディング長を調整する必要があります.
  - 23-48行目: 基本的には各層をインスタンス化するだけです.
    `pointwise_conv1` の出力次元数が入力の倍になっている点や,
    `depthwise_conv` の `groups` が入力次元数と同じになっている点に注意して
    ください.
- 53-70行目: 推論処理.
  59-60行目: Causal convolution の場合は,パディング長を大きくした分余計な信号
  が含まれるので除去しています.

5.2 Conformer encoder layer

次に下記のコードで,encoder層を実装します.

  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
class ConformerEncoderLayer(nn.Module):
    def __init__(self,
                 dim_model,
                 num_heads,
                 dim_ffw,
                 dropout,
                 activation,
                 norm_type_sattn,
                 norm_type_ffw,
                 norm_eps,
                 add_bias,
                 conv_kernel_size=3,
                 conv_activation="swish",
                 conv_norm_type="layer",
                 conv_padding_mode="zeros",
                 conv_causal=False,
                 conv_layout="post"):
        super().__init__()
        assert conv_layout in ["pre", "post"]
        self.conv_layout = conv_layout

        self.fc_factor = 0.5

        # =====================================================================
        # First half PFFN.
        # =====================================================================
        self.norm_ffw1 = create_norm(norm_type_ffw, dim_model, norm_eps, add_bias)
        self.ffw1 = PositionwiseFeedForward(
            dim_model=dim_model,
            dim_ffw=dim_ffw,
            dropout=dropout,
            activation=activation,
            add_bias=add_bias)
        # =====================================================================
        # MHA.
        # =====================================================================
        self.norm_sattn = create_norm(norm_type_sattn, dim_model, norm_eps, add_bias)
        self.self_attn = MultiheadAttention(
            key_dim=dim_model,
            query_dim=dim_model,
            att_dim=dim_model,
            out_dim=dim_model,
            num_heads=num_heads,
            dropout=dropout,
            add_bias=add_bias)
        # =====================================================================
        # Conv module.
        # =====================================================================
        self.norm_conv = create_norm(conv_norm_type, dim_model, norm_eps, add_bias)
        self.conv = ConformerConvBlock(
            dim_model=dim_model,
            kernel_size=conv_kernel_size,
            activation=conv_activation,
            norm_type=conv_norm_type,
            padding_mode=conv_padding_mode,
            causal=conv_causal)

        # =====================================================================
        # Second half PFFN.
        # =====================================================================
        self.norm_ffw2 = create_norm(norm_type_ffw, dim_model, norm_eps, add_bias)
        self.ffw2 = PositionwiseFeedForward(
            dim_model=dim_model,
            dim_ffw=dim_ffw,
            dropout=dropout,
            activation=activation,
            add_bias=add_bias)

        self.dropout = nn.Dropout(p=dropout)

        # To store attention weights.
        self.attw = None

    def _forward_preconv(self, feature, san_mask):
        #################################################
        # First half PFFN.
        #################################################
        # `[N, qlen, dim_model]`
        residual = feature
        feature = apply_norm(self.norm_ffw1, feature)
        feature = self.ffw1(feature)
        feature = self.fc_factor * self.dropout(feature) + residual

        #################################################
        # Conv.
        #################################################
        # `[N, qlen, dim_model]`
        residual = feature
        feature = apply_norm(self.norm_conv, feature)
        feature = self.conv(feature)
        feature = self.dropout(feature) + residual

        #################################################
        # MHA.
        #################################################
        # `[N, qlen, dim_model]`
        residual = feature
        feature = apply_norm(self.norm_sattn, feature)
        feature, self.attw = self.self_attn(
            key=feature,
            value=feature,
            query=feature,
            mask=san_mask)
        feature = self.dropout(feature) + residual

        #################################################
        # Second half PFFN.
        #################################################
        # `[N, qlen, dim_model]`
        residual = feature
        feature = apply_norm(self.norm_ffw2, feature)
        feature = self.ffw2(feature)
        feature = self.fc_factor * self.dropout(feature) + residual
        return feature

    def _forward_postconv(self, feature, san_mask):
        #################################################
        # First half PFFN.
        #################################################
        # `[N, qlen, dim_model]`
        residual = feature
        feature = apply_norm(self.norm_ffw1, feature)
        feature = self.ffw1(feature)
        feature = self.fc_factor * self.dropout(feature) + residual

        #################################################
        # MHA.
        #################################################
        # `[N, qlen, dim_model]`
        residual = feature
        feature = apply_norm(self.norm_sattn, feature)
        feature, self.attw = self.self_attn(
            key=feature,
            value=feature,
            query=feature,
            mask=san_mask)
        feature = self.dropout(feature) + residual

        #################################################
        # Conv.
        #################################################
        # `[N, qlen, dim_model]`
        residual = feature
        feature = apply_norm(self.norm_conv, feature)
        feature = self.conv(feature)
        feature = self.dropout(feature) + residual

        #################################################
        # Second half PFFN.
        #################################################
        # `[N, qlen, dim_model]`
        residual = feature
        feature = apply_norm(self.norm_ffw2, feature)
        feature = self.ffw2(feature)
        feature = self.fc_factor * self.dropout(feature) + residual
        return feature

    def forward(self,
                feature,
                causal_mask=None,
                src_key_padding_mask=None):
        bsize, qlen = feature.shape[:2]
        if src_key_padding_mask is not None:
            san_mask = make_san_mask(src_key_padding_mask, causal_mask)
        elif causal_mask is not None:
            san_mask = causal_mask
        else:
            san_mask = None

        if self.conv_layout == "pre":
            feature = self._forward_preconv(feature, san_mask)
        else:
            feature = self._forward_postconv(feature, san_mask)
        return feature
【コード解説】
- 引数
  - dim_model: 入力特徴量の次元数
  - num_heads: MHAのヘッド数
  - dim_ffw: PFFNの内部特徴次元数
  - dropout: Dropout層の欠落率
  - activation: 活性化関数の種別を指定 [relu/gelu/swish/silu/mish]
  - norm_type_sattn: MHSAブロックの正規化層種別を指定 [batch/layer]
  - norm_type_ffw: PFFNブロックの正規化層種別を指定 [batch/layer]
  - norm_eps: 正規化層内で0除算を避けるための定数
  - add_bias: Trueの場合,線形変換層と正規化層にバイアス項を適用.
    ただし,LN層がバイアス項に対応していない場合 (古いPyTorch) は無視します.
  - conv_kernel_size: Convolution module のカーネルサイズ
  - conv_activation: Convolution module の活性化関数
  - conv_norm_type: Convolution module の正規化層種別
  - conv_paddin_mode: Convolution module のパディング設定
  - conv_causal: True の場合,Convolution module で Causal convolution を適用
  - conv_layout: Convolution module をMHSA の前後どちらに配置するか [pre/post]
- 18-72行目: 初期化処理.各層を順にインスタンス化しています.
- 74-114行目: MHSA の前に Convolution module を配置した場合の処理
- 116-156行目: MHSA の後ろに Convolution module を配置した場合の処理
- 158-174行目: 推論処理.
  - 163-168行目: MHSA用のマスキング配列を作成
  - 170-174行目: Convolution module の配置に従って推論処理を実行

5.3 認識モデル

次のコードで,認識モデル全体を実装します.
基本的には Macaron Net と同様 (第4.4項をご参照ください) です.
ただし,Convolution module 用の引数が加わっています.
また,45-60行目で ConformerEncoderLayer を呼び出している点に注意してください.

  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
class ConformerEnISLR(nn.Module):
    def __init__(self,
                 in_channels,
                 inter_channels,
                 out_channels,
                 activation="relu",
                 pooling_type="none",
                 tren_num_layers=1,
                 tren_num_heads=1,
                 tren_dim_ffw=256,
                 tren_dropout_pe=0.1,
                 tren_dropout=0.1,
                 tren_norm_type_sattn="layer",
                 tren_norm_type_ffw="layer",
                 tren_norm_type_tail="layer",
                 tren_norm_eps=1e-5,
                 tren_add_bias=True,
                 tren_add_tailnorm=True,
                 conv_kernel_size=3,
                 conv_activation="swish",
                 conv_norm_type="layer",
                 conv_padding_mode="zeros",
                 conv_causal=False,
                 conv_layout="post"):
        super().__init__()

        # Feature extraction.
        self.linear = nn.Linear(in_channels, inter_channels)
        self.activation = select_reluwise_activation(activation)

        if pooling_type == "none":
            self.pooling = Identity()
        elif pooling_type == "average":
            self.pooling = nn.AvgPool2d(
                kernel_size=[2, 1],
                stride=[2, 1],
                padding=0)
        elif pooling_type == "max":
            self.pooling = nn.MaxPool2d(
                kernel_size=[2, 1],
                stride=[2, 1],
                padding=0)

        # Transformer-Encoder.
        enlayer = ConformerEncoderLayer(
            dim_model=inter_channels,
            num_heads=tren_num_heads,
            dim_ffw=tren_dim_ffw,
            dropout=tren_dropout,
            activation=activation,
            norm_type_sattn=tren_norm_type_sattn,
            norm_type_ffw=tren_norm_type_ffw,
            norm_eps=tren_norm_eps,
            add_bias=tren_add_bias,
            conv_kernel_size=conv_kernel_size,
            conv_activation=conv_activation,
            conv_norm_type=conv_norm_type,
            conv_padding_mode=conv_padding_mode,
            conv_causal=conv_causal,
            conv_layout=conv_layout)
        self.tr_encoder = TransformerEncoder(
            encoder_layer=enlayer,
            num_layers=tren_num_layers,
            dim_model=inter_channels,
            dropout_pe=tren_dropout_pe,
            norm_type_tail=tren_norm_type_tail,
            norm_eps=tren_norm_eps,
            norm_first=True,  # Fixed for MacaronNet.
            add_bias=tren_add_bias,
            add_tailnorm=tren_add_tailnorm)

        self.head = GPoolRecognitionHead(inter_channels, out_channels)

    def forward(self,
                feature,
                feature_causal_mask=None,
                feature_pad_mask=None):
        # Feature extraction.
        # `[N, C, T, J] -> [N, T, C, J] -> [N, T, C*J] -> [N, T, C']`
        N, C, T, J = feature.shape
        feature = feature.permute([0, 2, 1, 3])
        feature = feature.reshape(N, T, -1)

        feature = self.linear(feature)
        if torch.isnan(feature).any():
            raise ValueError()
        feature = self.activation(feature)
        if torch.isnan(feature).any():
            raise ValueError()

        # Apply pooling.
        feature = self.pooling(feature)
        if feature_pad_mask is not None:
            # Cast to apply pooling.
            feature_pad_mask = feature_pad_mask.to(feature.dtype)
            feature_pad_mask = self.pooling(feature_pad_mask.unsqueeze(-1)).squeeze(-1)
            # Binarization.
            # This removes averaged signals with padding.
            feature_pad_mask = feature_pad_mask > 0.5
            if feature_causal_mask is not None:
                feature_causal_mask = make_causal_mask(feature_pad_mask)

        feature = self.tr_encoder(
            feature=feature,
            causal_mask=feature_causal_mask,
            src_key_padding_mask=feature_pad_mask)
        if torch.isnan(feature).any():
            raise ValueError()

        # `[N, T, C] -> [N, C, T]`
        logit = self.head(feature.permute([0, 2, 1]), feature_pad_mask)
        if torch.isnan(feature).any():
            raise ValueError()
        return logit

5.4 動作チェック

認識モデルの実装ができましたので,動作確認をしていきます.
次のコードでデータセットから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)

次のコードで前処理を定義します.
今回は「手話認識入門」で紹介した,各種のデータ拡張を実装します.

 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
_, use_landmarks = get_fullbody_landmarks()
use_features = ["x", "y"]
swap_pairs, _ = get_fullbody_swappairs()

trans_select_feature = SelectLandmarksAndFeature(landmarks=use_landmarks, features=use_features)
trans_repnan = ReplaceNan()
trans_norm = PartsBasedNormalization(align_mode="framewise", scale_mode="unique")

trans_hflip = RandomHorizontalFlip(
    apply_ratio=0.5,
    num_joints=len(use_landmarks),
    swap_pairs=swap_pairs,
    flip_center=0.5,
    feature_dim=len(use_features),
    include_conf=False
)
trans_saffine = RandomAffineTransform2D(
    apply_ratio=0.5,
    center_joints=[0, 2],
    target_joints=np.arange(0, len(use_landmarks)),
    trans_range=[-0.1, 0.1],
    scale_range=[1.0/1.5, 1.5],
    rot_range=[-30, 30],
    skew_range=[-30, 30]
)
trans_snoise = RandomNoise(
    apply_ratio=0.5,
    scale_range=[1e-3, 1e-2],
    scale_unit="asis",
    noise_type="uniform",
    target_joints=np.arange(0, len(use_landmarks)),
    feature_dim=len(use_features),
    include_conf=False
)
trans_clip =  RandomClip(
    apply_ratio=0.5,
    clip_range=[0.4, 0.6],
    min_apply_size=10
)
trans_twarp = RandomTimeWarping(
    apply_ratio=0.5,
    scale_range=[0.5, 2.0],
    min_apply_size=10
)
trans_drop = RandomDropParts(apply_ratio=0.5)

pre_transforms = Compose([trans_select_feature,
                          trans_repnan])

train_transforms = Compose([
    trans_hflip,
    trans_saffine,
    trans_snoise,
    trans_norm,
    trans_clip,
    trans_twarp,
    trans_drop,
    ToTensor()
])
val_transforms = Compose([trans_norm, ToTensor()])
test_transforms = Compose([trans_norm, ToTensor()])

47-61行目の実装構成が今まで異なる点に注意してください.
まず,追跡点の正規化処理 (trans_norm) を 固定の前処理 (pre_transforms) から動的な前処理へ移動しています.
データ拡張の順番は色々と考えられますが今回は,画像空間系のデータ拡張 \(\rightarrow\) 追跡点の正規化 \(\rightarrow\) 時間系のデータ拡張の順で実行するようにしています.

次のコードで,前処理を適用したHDF5DatasetとDataLoaderをインスタンス化し,データを取り出します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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)

dataset = HDF5Dataset(hdf5_files, pre_transforms=pre_transforms, transforms=transforms)

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, 6, 130])

次のコードでモデルをインスタンス化して,動作チェックをします.
追跡点抽出の結果,入力追跡点数は130で,各追跡点はXY座標値を持っていますので,入力次元数は260になります.
出力次元数は単語数なので10になります.
また,Conformer層の入力次元数は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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# 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"
pooling_type = "none"
tren_num_layers = 2
tren_num_heads = 2
tren_dim_ffw = 256
tren_dropout_pe = 0.1
tren_dropout = 0.1
tren_norm_type_sattn = "layer"
tren_norm_type_ffw = "layer"
tren_norm_type_tail = "layer"
tren_norm_eps = 1e-5
tren_norm_first = True  # For Transformer.
tren_add_bias = True
tren_add_tailnorm = True
conv_kernel_size = 3
conv_activation = "swish"
conv_norm_type = "batch"
conv_padding_mode = "zeros"
conv_causal = False
conv_layout = "post"

model = ConformerEnISLR(in_channels=in_channels,
                        inter_channels=inter_channels,
                        out_channels=out_channels,
                        activation=activation,
                        pooling_type=pooling_type,
                        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_norm_type_sattn=tren_norm_type_sattn,
                        tren_norm_type_ffw=tren_norm_type_ffw,
                        tren_norm_type_tail=tren_norm_type_tail,
                        tren_norm_eps=tren_norm_eps,
                        tren_add_bias=tren_add_bias,
                        tren_add_tailnorm=tren_add_tailnorm,
                        conv_kernel_size=conv_kernel_size,
                        conv_activation=conv_activation,
                        conv_norm_type=conv_norm_type,
                        conv_padding_mode=conv_padding_mode,
                        conv_causal=conv_causal,
                        conv_layout=conv_layout)
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)
ConformerEnISLR(
  (linear): Linear(in_features=260, out_features=64, bias=True)
  (activation): ReLU()
  (pooling): Identity()
  (tr_encoder): TransformerEncoder(
    (pos_encoder): PositionalEncoding(
      (dropout): Dropout(p=0.1, inplace=False)
    )
    (layers): ModuleList(
      (0-1): 2 x ConformerEncoderLayer(
        (norm_ffw1): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
        (ffw1): 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()
        )
        (norm_sattn): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
        (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)
        )
        (norm_conv): BatchNorm1d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (conv): ConformerConvBlock(
          (pointwise_conv1): Conv1d(64, 128, kernel_size=(1,), stride=(1,))
          (depthwise_conv): Conv1d(64, 64, kernel_size=(3,), stride=(1,), padding=(1,), groups=64)
          (norm): BatchNorm1d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          (activation): SiLU()
          (pointwise_conv2): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
        )
        (norm_ffw2): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
        (ffw2): 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)
      )
    )
    (norm_tail): 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, 6, 6) (2, 2, 6, 6)

6. 学習と評価の実行

6.1 共通設定

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 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
label_smoothing = 0.1

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 cpu for computation.

次のコードで学習・バリデーション・評価処理それぞれのための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)

6.2 学習・評価の実行

次のコードでモデルをインスタンス化します.
26行目に示すように,今回はラベルスムージングを有効にしています.

 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
model_conformer = ConformerEnISLR(
    in_channels=in_channels,
    inter_channels=inter_channels,
    out_channels=out_channels,
    activation=activation,
    pooling_type=pooling_type,
    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_norm_type_sattn=tren_norm_type_sattn,
    tren_norm_type_ffw=tren_norm_type_ffw,
    tren_norm_type_tail=tren_norm_type_tail,
    tren_norm_eps=tren_norm_eps,
    tren_add_bias=tren_add_bias,
    tren_add_tailnorm=tren_add_tailnorm,
    conv_kernel_size=conv_kernel_size,
    conv_activation=conv_activation,
    conv_norm_type=conv_norm_type,
    conv_padding_mode=conv_padding_mode,
    conv_causal=conv_causal,
    conv_layout=conv_layout)
print(model_conformer)

loss_fn = nn.CrossEntropyLoss(reduction="mean", label_smoothing=label_smoothing)
optimizer = torch.optim.Adam(model_conformer.parameters(), lr=lr)
ConformerEnISLR(
  (linear): Linear(in_features=260, out_features=64, bias=True)
  (activation): ReLU()
  (pooling): Identity()
  (tr_encoder): TransformerEncoder(
    (pos_encoder): PositionalEncoding(
      (dropout): Dropout(p=0.1, inplace=False)
    )
    (layers): ModuleList(
      (0-1): 2 x ConformerEncoderLayer(
        (norm_ffw1): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
        (ffw1): 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()
        )
        (norm_sattn): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
        (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)
        )
        (norm_conv): BatchNorm1d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (conv): ConformerConvBlock(
          (pointwise_conv1): Conv1d(64, 128, kernel_size=(1,), stride=(1,))
          (depthwise_conv): Conv1d(64, 64, kernel_size=(3,), stride=(1,), padding=(1,), groups=64)
          (norm): BatchNorm1d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          (activation): SiLU()
          (pointwise_conv2): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
        )
        (norm_ffw2): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
        (ffw2): 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)
      )
    )
    (norm_tail): 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_conformer.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_conformer, loss_fn, optimizer, device)
    val_loss = val_loop(val_dataloader, model_conformer, loss_fn, device)
    val_losses.append(val_loss)

    if (epoch+1) % eval_every_n_epochs == 0:
        acc = test_loop(test_dataloader, model_conformer, device)
        test_accs.append(acc)
train_losses_conformer = np.array(train_losses)
val_losses_conformer = np.array(val_losses)
test_accs_conformer = np.array(test_accs)

print(f"Minimum validation loss:{val_losses_conformer.min()} at {np.argmin(val_losses_conformer)+1} epoch.")
print(f"Maximum accuracy:{test_accs_conformer.max()} at {np.argmax(test_accs_conformer)*eval_every_n_epochs+1} epoch.")
--------------------------------------------------------------------------------
Epoch 1
Start training.
loss:4.260931 [    0/ 3881]
loss:1.925219 [ 3200/ 3881]
Done. Time:14.010988099000002
Training performance: 
 Avg loss:2.293249

Start validation.
Done. Time:0.3888901189999956
Validation performance: 
 Avg loss:2.106192

Start evaluation.
Done. Time:1.8480841040000087
Test performance: 
 Accuracy:27.0%
--------------------------------------------------------------------------------
...
--------------------------------------------------------------------------------
Epoch 50
Start training.
loss:0.943248 [    0/ 3881]
loss:0.843957 [ 3200/ 3881]
Done. Time:10.065681318999964
Training performance: 
 Avg loss:0.884365

Start validation.
Done. Time:0.3884928630000104
Validation performance: 
 Avg loss:0.820092

Start evaluation.
Done. Time:1.6283517409999604
Test performance: 
 Accuracy:83.0%
Minimum validation loss:0.7889328258378165 at 48 epoch.
Maximum accuracy:89.0 at 49 epoch.

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


今回は Conformer を用いた孤立手話単語認識モデルを紹介しましたが,如何でしたでしょうか?
Conformer は音声認識分野で発表された手法で,実装もそこまで難しくないことから一時結構流行っていた印象です.
(Google 社が発表したという点が大きいかもしれませんが)

音声認識は,(i) 時系列を扱う,(ii) 入力が複雑な特徴を持つ,(iii) 連続的な特徴量 → 離散的な特徴量 (テキスト) への変換を行う,など手話認識と共通している点が多いです.

そこから,手話認識では音声認識の手法を取り込んで改良を施すというアプローチが取られる傾向があります.
手話認識モデルの改良に悩んだ場合は,音声認識分野を参考にしてみると良いかもしれません.

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