実践手話認識 - モデル開発編 補足: Pydanticを用いてTransformerベース孤立手話単語認識モデルをリファクタリング

This image is generated with ChatGPT-4 Omni, and edited by the author.
作成日:2024年10月24日(木) 00:00
最終更新日:2024年10月25日(金) 10:14
カテゴリ:手話言語処理
タグ:  実践手話認識 孤立手話単語認識 Pydantic Transformer Python

Pydanticを用いてTransformerベースの孤立手話単語認識モデルをリファクタリングしましたので,実装コードを紹介します.

こんにちは.高山です.
今回の記事は,実践手話認識-モデル開発編第六回の補足になります.

第六回からは,Pydanticを用いた NNモデルの実装 に即してコードを実装しています.
Pydantic の BaseModel を継承したクラスにモデルのハイパーパラメータをまとめることで,下記のようにコードを改善することができます.

  • モデル,レイヤに冗長な引数が並ぶことを避けることができます.
  • Pydantic のデータ検証機能によって,不正な値がモデルに渡されることを防ぐことができます.
  • Pydantic のデータ変換機能によって,ハイパーパラメータの保存,読み込みが簡単に実装できます.
  • 一貫したスタイルでモデル,レイヤが実装できます.

第六回の記事で実験に使用したコードでは,ローカル特徴抽出の実装と同時に Transformer encoder のリファクタリング (動作を保ったまま,コードの品質を改善することを指します) も行っています.
Transformer encoder リファクタリングは第六回の記事の主要点では無いので,本記事で紹介したいと思います.

今回解説するスクリプトはGitHub上に公開しています
本記事の紹介箇所は第4節 "Rafactor Transformer ISLR model" です.

1. Pydantic モデルの設定

まず最初に,下記のコードで Pydantic モデルの設定を行います.

1
2
class ConfiguredModel(BaseModel):
    model_config = ConfigDict(extra="forbid")

上記の設定は,クラスで未定義の変数が入力された場合にエラーとする設定です.
(デフォルトでは無視する設定になっています)

ConfiguredModel を継承したクラスにパラメータを実装することで,Pydantic の検証機能を備えたパラメータクラスが実装できます.

2. Positional encoding

では,Transformer を構成するレイヤを実装していきます.
次のコードで Positional encoding の設定パラメータを実装します.

1
2
3
4
5
6
7
class PositionalEncodingSettings(ConfiguredModel):
    dim_model: int = 64
    dropout: float = 0.1
    max_len: int = 5000

    def build_layer(self):
        return PositionalEncoding(self)
【コード解説】
- 引数
  - dim_model: 入力特徴量の次元数
  - dropout: Dropout層の欠落率
  - max_len: 位置信号を生成する長さ (入力可能な最大長)

パラメータクラスには build_layer() メソッドを実装して,自身のパラメータからレイヤを生成できるようにしています.
この処理によって,呼び出し側でレイヤクラスを読み込む必要が無くなります.

パラメータクラスを用いると,レイヤクラスは下記のように実装できます.

 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
class PositionalEncoding(nn.Module):
    def __init__(self,
                 settings):
        super().__init__()
        assert isinstance(settings, PositionalEncodingSettings)
        self.settings = settings

        self.dim_model = settings.dim_model
        # Compute the positional encodings once in log space.
        pose = torch.zeros(settings.max_len, settings.dim_model,
            dtype=torch.float32)
        position = torch.arange(0, settings.max_len,
            dtype=torch.float32).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, settings.dim_model, 2).float()
                             * -(math.log(10000.0) / settings.dim_model))
        pose[:, 0::2] = torch.sin(position * div_term)
        pose[:, 1::2] = torch.cos(position * div_term)
        self.register_buffer("pose", pose)

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

    def forward(self,
                feature):
        feature = feature + self.pose[None, :feature.shape[1], :]
        feature = self.dropout(feature)
        return feature

__init__() の引数がパラメータクラスになっていることが分かります.
細かな処理については手話認識入門第九回 で説明していますので割愛させていただきます.

3. Multi-head attention

次のコードで Multi-head attention の設定パラメータを実装します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class MultiheadAttentionSettings(ConfiguredModel):
    key_dim: int = 64
    query_dim: int = 64
    att_dim: int = 64
    out_dim: int = 64
    num_heads: int = 1
    dropout: float = 0.1
    add_bias: bool = True

    def build_layer(self):
        return MultiheadAttention(self)
【コード解説】
- 引数:
  - key_dim: Key値の入力次元数.Self-attentionでは `query_dim` と同じ値にします.
    Cross-attentionでは主要入力 (Decoder側の入力) の次元数を設定します.
  - query_dim: Query値の入力次元数.Self-attentionでは `key_dim` と同じ値にします.
    Cross-attentionでは補助入力 (Encoder側の出力) の次元数を設定します.
  - att_dim: 内部処理の特徴量次元数.`num_heads` で割り切れる値なければなりません.
  - out_dim: 出力次元数
  - num_heads: 異なるアテンション重みを計算するためのヘッド数
  - dropout: Dropout層の欠落率
  - add_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
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
class MultiheadAttention(nn.Module):
    def __init__(self,
                 settings):
        super().__init__()
        assert isinstance(settings, MultiheadAttentionSettings)
        self.settings = settings

        assert settings.att_dim % settings.num_heads == 0
        self.head_dim = settings.att_dim // settings.num_heads
        self.num_heads = settings.num_heads
        self.scale = math.sqrt(self.head_dim)

        self.w_key = nn.Linear(settings.key_dim, settings.att_dim,
            bias=settings.add_bias)
        self.w_value = nn.Linear(settings.key_dim, settings.att_dim,
            bias=settings.add_bias)
        self.w_query = nn.Linear(settings.query_dim, settings.att_dim,
            bias=settings.add_bias)

        self.w_out = nn.Linear(settings.att_dim, settings.out_dim, bias=settings.add_bias)

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

        self.neg_inf = None

        self.qkv_same_dim = settings.key_dim == settings.query_dim
        self.reset_parameters(settings.add_bias)

    def reset_parameters(self, add_bias):
        """Initialize parameters with Xavier uniform distribution.

        # NOTE: For this initialization, please refer
        https://github.com/pytorch/fairseq/blob/master/fairseq/modules/multihead_attention.py  # pylint: disable=line-too-long

        """
        if self.qkv_same_dim:
            nn.init.xavier_uniform_(self.w_key.weight, gain=1 / math.sqrt(2))
            nn.init.xavier_uniform_(self.w_value.weight, gain=1 / math.sqrt(2))
            nn.init.xavier_uniform_(self.w_query.weight, gain=1 / math.sqrt(2))
        else:
            nn.init.xavier_uniform_(self.w_key.weight)
            nn.init.xavier_uniform_(self.w_value.weight)
            nn.init.xavier_uniform_(self.w_query.weight)
        nn.init.xavier_uniform_(self.w_out.weight)
        if add_bias:
            nn.init.constant_(self.w_key.bias, 0.)
            nn.init.constant_(self.w_value.bias, 0.)
            nn.init.constant_(self.w_query.bias, 0.)
            nn.init.constant_(self.w_out.bias, 0.)

    def forward(self,
                key: torch.Tensor,
                value: torch.Tensor,
                query: torch.Tensor,
                mask: torch.Tensor):
        if self.neg_inf is None:
            self.neg_inf = float(np.finfo(
                torch.tensor(0, dtype=key.dtype).numpy().dtype).min)

        bsize, klen = key.size()[: 2]
        qlen = query.size(1)

        # key: `[N, klen, kdim] -> [N, klen, adim] -> [N, klen, H, adim/H(=hdim)]`
        # value: `[N, klen, vdim] -> [N, klen, adim] -> [N, klen, H, adim/H(=hdim)]`
        # query: `[N, qlen, qdim] -> [N, qlen, adim] -> [N, qlen, H, adim/H(=hdim)]`
        key = self.w_key(key).reshape([bsize, -1, self.num_heads, self.head_dim])
        value = self.w_value(value).reshape([bsize, -1, self.num_heads, self.head_dim])
        query = self.w_query(query).reshape([bsize, -1, self.num_heads, self.head_dim])

        # qk_score: `[N, qlen, H, hdim] x [N, klen, H, hdim] -> [N, qlen, klen, H]`
        qk_score = torch.einsum("bihd,bjhd->bijh", (query, key)) / self.scale

        # Apply mask.
        if mask is not None:
            # `[N, qlen, klen] -> [N, qlen, klen, H]`
            mask = mask.unsqueeze(3).repeat([1, 1, 1, self.num_heads])
            mask_size = (bsize, qlen, klen, self.num_heads)
            assert mask.size() == mask_size, f"{mask.size()}:{mask_size}"
            # Negative infinity should be 0 in softmax.
            qk_score = qk_score.masked_fill_(mask == 0, self.neg_inf)
        # Compute attention weight.
        attw = torch.softmax(qk_score, dim=2)
        attw = self.dropout_attn(attw)

        # cvec: `[N, qlen, klen, H] x [N, qlen, h, hdim] -> [N, qlen, H, hdim]
        # -> [N, qlen, H * hdim]`
        cvec = torch.einsum("bijh,bjhd->bihd", (attw, value))
        cvec = cvec.reshape([bsize, -1, self.num_heads * self.head_dim])
        cvec = self.w_out(cvec)
        # attw: `[N, qlen, klen, H]` -> `[N, H, qlen, klen]`
        attw = attw.permute(0, 3, 1, 2)
        return cvec, attw

4. Position-wise feed-forward

次のコードで Position-wise feed-foward の設定パラメータを実装します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class PositionwiseFeedForwardSettings(ConfiguredModel):
    dim_model: int = 64
    dim_pffn: int = 256
    dropout: float = 0.1
    activation: str = Field(default="relu",
        pattern=r"relu|gelu|swish|silu|mish|geluacc|tanhexp")
    add_bias: bool = True

    def build_layer(self):
        return PositionwiseFeedForward(self)
【コード解説】
- 引数
  - dim_model: 入力特徴量の次元数
  - dim_pffn: 1層目の線形変換後の特徴量次元数
  - dropout: Dropout層の欠落率
  - activation: 活性化関数の種別を指定 [relu/gelu/swish/silu/mish/geluacc/tanhexp]
  - add_bias: Trueの場合,線形変換層にバイアス項を導入する

activation は入力値を実装済みの活性化関数名に制限したいので,Field を使って型を定義しています.
pattern に入力可能な文字列を正規表現で与えることで,値を制限できます.

パラメータクラスを用いると,レイヤクラスは下記のように実装できます.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class PositionwiseFeedForward(nn.Module):
    def __init__(self,
                 settings):
       super().__init__()
       assert isinstance(settings, PositionwiseFeedForwardSettings)

       self.w_1 = nn.Linear(settings.dim_model, settings.dim_pffn,
           bias=settings.add_bias)
       self.w_2 = nn.Linear(settings.dim_pffn, settings.dim_model,
           bias=settings.add_bias)

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

       self.activation = select_reluwise_activation(settings.activation)

    def forward(self, feature):
        feature = self.w_1(feature)
        feature = self.activation(feature)
        feature = self.dropout(feature)
        feature = self.w_2(feature)
        return feature

5. Transformer encoder layer

次のコードで Transformer encoder layer の設定パラメータを実装します.

 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
class TransformerEncoderLayerSettings(ConfiguredModel):
    dim_model: int = 64
    dim_pffn: int = 256
    activation: str = Field(default="relu",
        pattern=r"relu|gelu|swish|silu|mish|geluacc|tanhexp")
    norm_type_sattn: str = Field(default="layer", pattern=r"layer|batch")
    norm_type_pffn: str = Field(default="layer", pattern=r"layer|batch")
    norm_eps: float = 1e-5
    norm_first: bool = True
    dropout: float = 0.1

    mhsa_settings: MultiheadAttentionSettings = Field(
        default_factory=lambda: MultiheadAttentionSettings())
    pffn_settings: PositionwiseFeedForwardSettings = Field(
        default_factory=lambda: PositionwiseFeedForwardSettings())

    def model_post_init(self, __context):
        # Adjust mhsa_settings.
        self.mhsa_settings.key_dim = self.dim_model
        self.mhsa_settings.query_dim = self.dim_model
        self.mhsa_settings.att_dim = self.dim_model
        self.mhsa_settings.out_dim = self.dim_model
        # Adjust pffn_settings.
        self.pffn_settings.dim_model = self.dim_model
        self.pffn_settings.dim_pffn = self.dim_pffn
        self.pffn_settings.activation = self.activation

        # Propagate.
        self.mhsa_settings.model_post_init(__context)
        self.pffn_settings.model_post_init(__context)

    def build_layer(self):
        if self.norm_first:
            layer = PreNormTransformerEncoderLayer(self)
        else:
            layer = PostNormTransformerEncoderLayer(self)
        return layer
【コード解説】
- 引数
  - dim_model: 入力特徴量の次元数
  - dim_pffn: PFFNの内部特徴次元数
  - activation: 活性化関数の種別を指定 [relu/gelu/swish/silu/mish/geluacc/tanhexp]
  - norm_type_sattn: MHSAの正規化層種別を指定
  - norm_type_PFFN: PFFNの正規化層種別を指定
  - norm_eps: LN層内で0除算を避けるための定数
  - norm_first: Trueの場合,Pre-LN構成を用いる
  - dropout: Dropout層の欠落率
  - mhsa_settings: MHSAのパラメータクラス
  - pffn_settings: PFFNのパラメータクラス

MHSA と PFFN の設定パラメータを,それぞれ mhsa_settingspffn_settings として保持しています.
これら子レイヤのパラメータのうち,dim_model は親レイヤと共通の値を設定する必要があります.
また,dim_pffnactivation はパラメータ探索の過程で頻繁に変更されます.
そこで親レイヤのパラメータにも同名の変数を用意し,model_post_init() 内で親から子へ値を受け渡して設定が連動するようにしています.

少し実装の手間が増えますが,この処理によってアプリケーション側ではいくつかのよく使う値を設定するだけで,他の値は自動的に連動して変化するような処理を行うことができます.

以前の実装では,Pre-LN 構成と標準構成の Transformer を一つのクラスに実装していました.
この実装だとコードが冗長になるのと,print() などでモデルを表示した場合に違いが分からなかったので,実装を分けています.
そのため,build_layer() 内で norm_first の値に応じてインスタンス化するレイヤを切り替えるようにしています.

では,レイヤクラスを実装していきます.
まず,Pre-LN 構成と標準構成の Transformer で共通して行う,マスク作成処理を下記の関数にまとめます.

1
2
3
4
5
6
7
8
9
def create_encoder_mask(src_key_padding_mask,
                        causal_mask):
    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
    return san_mask

パラメータクラスを用いると,Pre-LN 構成の Transformer は下記のように実装できます.

 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
class PreNormTransformerEncoderLayer(nn.Module):
    """Pre-normalization structure.

    For the details, please refer
    https://arxiv.org/pdf/2002.04745v1.pdf
    """
    def __init__(self,
                 settings):
        super().__init__()
        assert isinstance(settings, TransformerEncoderLayerSettings)
        assert settings.norm_first is True
        self.settings = settings

        #################################################
        # MHSA.
        #################################################
        self.norm_sattn = create_norm(
            settings.norm_type_sattn, settings.dim_model, settings.norm_eps,
            settings.mhsa_settings.add_bias)
        self.self_attn = settings.mhsa_settings.build_layer()

        #################################################
        # PFFN.
        #################################################
        self.norm_pffn = create_norm(settings.norm_type_pffn, settings.dim_model,
            settings.norm_eps, settings.pffn_settings.add_bias)
        self.pffn = settings.pffn_settings.build_layer()

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

        # To store attention weights.
        self.attw = None

    def forward(self,
                feature,
                causal_mask=None,
                src_key_padding_mask=None):
        bsize, qlen = feature.shape[:2]
        san_mask = create_encoder_mask(src_key_padding_mask, causal_mask)

        #################################################
        # MHSA
        #################################################
        # `[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

        #################################################
        # PFFN
        #################################################
        residual = feature
        # `[N, qlen, dim_model]`
        feature = apply_norm(self.norm_pffn, feature)
        feature = self.pffn(feature)
        feature = self.dropout(feature) + residual

        return feature

MHSA と PFFN のインスタンス化処理は,各レイヤのパラメータクラスから build_layer() を呼び出すことで行っています (20, 27行目).
パラメータクラス側にビルド処理を実装しておくことで,レイヤクラスの実装を簡略化することができます.

同様に,パラメータクラスを用いると標準構成の Transformer は下記のように実装できます.

 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
class PostNormTransformerEncoderLayer(nn.Module):
    """Post-normalization structure (Standard).

    For the details, please refer
    https://arxiv.org/pdf/2002.04745v1.pdf
    """
    def __init__(self,
                 settings):
        super().__init__()
        assert isinstance(settings, TransformerEncoderLayerSettings)
        assert settings.norm_first is False
        self.settings = settings

        #################################################
        # MHSA.
        #################################################
        self.self_attn = settings.mhsa_settings.build_layer()
        self.norm_sattn = create_norm(
            settings.norm_type_sattn, settings.dim_model, settings.norm_eps,
            settings.mhsa_settings.add_bias)

        #################################################
        # PFFN.
        #################################################
        self.pffn = settings.pffn_settings.build_layer()
        self.norm_pffn = create_norm(settings.norm_type_pffn, settings.dim_model,
            settings.norm_eps, settings.pffn_settings.add_bias)

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

        # To store attention weights.
        self.attw = None

    def forward(self,
                feature,
                causal_mask=None,
                src_key_padding_mask=None):
        bsize, qlen = feature.shape[:2]
        san_mask = create_encoder_mask(src_key_padding_mask, causal_mask)

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

        #################################################
        # PFFN
        #################################################
        residual = feature
        # `[N, qlen, dim_model]`
        feature = self.pffn(feature)
        feature = self.dropout(feature) + residual
        feature = apply_norm(self.norm_pffn, feature)

        return feature

6. Transformer encoder block

次のコードで Transformer encoder block の設定パラメータを実装します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class TransformerEncoderSettings(ConfiguredModel):
    num_layers: int = 1
    norm_type_tail: str = Field(default="layer", pattern=r"layer|batch")
    norm_eps: float = 1e-5
    add_bias: bool = True
    add_tailnorm: bool = True

    pe_settings: PositionalEncodingSettings = Field(
        default_factory=lambda: PositionalEncodingSettings())

    def build_layer(self, encoder_layer):
        return TransformerEncoder(self, encoder_layer)
【コード解説】
- 引数
  - num_layers: encoder層の数を指定
  - norm_type_tail: ブロック末尾の正規化層種別を指定.[layer/batch]
  - norm_eps: LN層内で0除算を避けるための定数
  - add_bias: Trueの場合,末尾のLN層にバイアス項を適用する
  - add_tailnorm: この変数がTrueで,かつ,Pre-LN 構成の Transformer の場合は,
    ブロック末尾に LN層を追加
  - pe_settings: PE層のパラメータクラス

build_layer() には Encoder layer のインスタンスを渡して自身を作成するようにしています.
(今までの実装と同様の形態です)

パラメータクラスを用いると,レイヤクラスは下記のように実装できます.

 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
class TransformerEncoder(nn.Module):
    def __init__(self,
                 settings,
                 encoder_layer):
        super().__init__()
        assert isinstance(settings, TransformerEncoderSettings)
        dim_model = settings.pe_settings.dim_model
        assert dim_model == encoder_layer.settings.dim_model
        self.settings = settings

        self.pos_encoder = settings.pe_settings.build_layer()
        self.layers = nn.ModuleList([copy.deepcopy(encoder_layer) for _
            in range(settings.num_layers)])

        # Add LayerNorm at tail position.
        # This is applied only for pre-normalization structure because
        # post-normalization structure includes tail-normalization in encoder
        # layers.
        add_tailnorm0 = settings.add_tailnorm
        add_tailnorm1 = not isinstance(encoder_layer, PostNormTransformerEncoderLayer)
        if add_tailnorm0 and add_tailnorm1:
            self.norm_tail = create_norm(settings.norm_type_tail, dim_model,
                settings.norm_eps, settings.add_bias)
        else:
            self.norm_tail = Identity()

    def forward(self,
                feature,
                causal_mask,
                src_key_padding_mask):
        feature = self.pos_encoder(feature)
        for layer in self.layers:
            feature = layer(feature,
                            causal_mask,
                            src_key_padding_mask)
        feature = apply_norm(self.norm_tail, feature)
        return feature

7. 認識モデル

次のコードで認識モデル全体の設定パラメータを実装します.

 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
class TransformerEnISLRSettings(ConfiguredModel):
    in_channels: int = 64
    inter_channels: int = 64
    out_channels: int = 64
    activation: str = Field(default="relu",
        pattern=r"relu|gelu|swish|silu|mish|geluacc|tanhexp")
    pooling_type: str = Field(default="none", pattern=r"none|average|max")

    enlayer_settings: TransformerEncoderLayerSettings = Field(
        default_factory=lambda: TransformerEncoderLayerSettings())
    encoder_settings: TransformerEncoderSettings = Field(
        default_factory=lambda: TransformerEncoderSettings())

    head_settings: GPoolRecognitionHeadSettings = Field(
        default_factory=lambda: GPoolRecognitionHeadSettings())

    def model_post_init(self, __context):
        # Adjust enlayer_settings.
        self.enlayer_settings.dim_model = self.inter_channels
        self.enlayer_settings.activation = self.activation

        # Adjust head_settings.
        self.head_settings.in_channels = self.inter_channels
        self.head_settings.out_channels = self.out_channels

        # Propagate.
        self.enlayer_settings.model_post_init(__context)
        self.encoder_settings.model_post_init(__context)
        self.head_settings.model_post_init(__context)

    def build_layer(self, fext_settings=None):
        return TransformerEnISLR(self, fext_settings)
【コード解説】
- 引数
  - in_channels: 入力特徴量の次元数
  - inter_channels: 内部特徴量の次元数
  - out_channels: 出力特徴量の次元数.単語応答値を出力したいので,全単語数と同じにします.
  - activation: 活性化関数の種別を指定 [relu/gelu/swish/silu/mish/geluacc/tanhexp]
  - pooling_type: 特徴抽出をPoolingで縮小するかを指定.[none/average/max]
  - enlayer_settings: Encoder layer のパラメータクラス
  - encoder_settiongs: Encoder block のパラメータクラス
  - head_settings: 出力レイヤのパラメータクラス

基本的にはここまでに説明してきた処理と同じです.
第六回の記事で紹介したように,ここではローカル特徴抽出レイヤを切り替えられるようにしたいので,build_layer()fext_settings を渡すように実装しています.

パラメータクラスを用いると,認識モデルは下記のように実装できます.

 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
class TransformerEnISLR(nn.Module):
    def __init__(self,
                 settings,
                 fext_settings=None):
        super().__init__()
        assert isinstance(settings, TransformerEnISLRSettings)
        self.settings = settings
        self.fext_settings = fext_settings

        # Feature extraction.
        self.fext_module = create_fext_module(fext_settings)

        # Transformer-Encoder.
        enlayer = settings.enlayer_settings.build_layer()
        self.tr_encoder = settings.encoder_settings.build_layer(enlayer)

        self.head = settings.head_settings.build_layer()

    def _apply_fext(self,
                    feature):
        for layer in self.fext_module:
            feature = layer(feature)
        return feature

    def forward(self,
                feature,
                feature_causal_mask=None,
                feature_pad_mask=None):
        # Feature extraction.
        feature = self._apply_fext(feature)

        # `[N, C, T] -> [N, T, C]`
        feature = feature.permute([0, 2, 1])

        # Adjust mask size.
        if feature_pad_mask is not None:
            if feature_pad_mask.shape[-1] != feature.shape[1]:
                feature_pad_mask = F.interpolate(
                    feature_pad_mask.unsqueeze(1).float(),
                    feature.shape[1],
                    mode="nearest")
                feature_pad_mask = feature_pad_mask.squeeze(1) > 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

インスタンス化処理がかなりスッキリしました (^^).


今回は Pydantic を用いて Transformer ベースの孤立手話単語認識をリファクタリングしてみましたが,如何でしたでしょうか?
今回紹介したコードだけではピンと来ないかもしれませんが,細かくパラメータを変えていく実験用アプリケーションを組んだりすると便利さが実感できるかもしれません.

今回紹介した話が,同じようなことで悩んでいる方に何か参考になれば幸いです.