目次
こんにちは.高山です.
他の仕事の関係で,少し間が空いてしまいました(^^;).
先日の記事で告知しました手話入門記事の第十回になります.
今までは手話認識モデルの学習や実装を中心に説明してきました.
これから何回かは,データ拡張などの細かな認識性能改善技術を説明していこうと思います.
その手始めに今回は,ラベルスムージング (label smoothing)[Szegedy'16] を用いた認識性能の改善方法を紹介したいと思います.
ラベルスムージングは,非常に簡単な手法ながら効果が高い改善方法です.
孤立手話単語認識のようなオーソドックスなタスクに関しては,PyTorchに実装済みの手法がそのまま使えるのも良いところです.
今回解説するスクリプトはGitHub上に公開しています.
色々な実験を行っている都合で,CPUで動かした場合は結構時間がかるのでご注意ください.
- [Szegedy'16] C. Szegedy, et al., "Rethinking the Inception Architecture for Computer Vision," Proc. of the IEEE CVPR, available here, 2016.
更新履歴 (大きな変更のみ記載しています)
- 2024/07/29: Gitスクリプトのダウンロード元を
master
からv0.1
タグに変更 - 2024/07/23
- 第1節の構成を見直し
- 記事最終部の実験結果を削除して第2節に統合
1. ラベルスムージング
まず初めに,ラベルスムージングについて簡単に紹介します.
ラベルスムージングは,正則化 (regularization) と呼ばれるモデルの改善方法の一種です.
機械学習における正則化とは,モデルを何らかの方法で単純化する手法を指します.
正則化には様々な手法がありますが,モデルの推論結果にある程度の誤りを許容するアプローチは典型的な手法の一つです.
この手法はモデルが学習データに過度に当てはまることを抑制して,過学習を防ぐ効果があります.
ラベルスムージングは,上記の推論結果に誤りを許容するアプローチの一つです.
ラベルスムージングの処理工程を図1に示します.
![図1: ラベルスムージングの処理工程](https://takayama-rado.com/images/articles/slr_tutorial_simple_transformer_ls/overview.png)
図1の左端に示しているフローチャートは孤立手話単語認識の学習時の推論処理で,入力 \(\boldsymbol{x} \in \boldsymbol{R}^{N \times T \times *}\) を受けとり,出力 \(\hat{\boldsymbol{Y}} \in \boldsymbol{R}^{N \times |L|}\) を返します.
ここで \(N, T, *\) はそれぞれ,バッチ内のサンプル次元,時間次元,任意の特徴次元を示します.
\(|L|\) は認識対象の単語数を示します.
図1 中央上側のグラフに示すように,\(\hat{\boldsymbol{Y}}\) は各単語の推論確率を示す分布になります.
図1 中央下側のグラフに示すように,正解データ \(\boldsymbol{Y}\) は正解ラベルだけが \(1\) となり,他は \(0\) となる分布になります.
このような離散データの表現方法を One-hot encoding と呼びます.
孤立手話単語認識の学習では,\(\boldsymbol{Y}\) と \(\hat{\boldsymbol{Y}}\) を入力として,クロスエントロピー誤差を用いてロスを計算し,モデルを学習します.
ラベルスムージングを用いない場合は,モデルに対して"常に \(100%\) の確信度を持って推論しなさい" と言っている状態に等しいです.
モデルはどうにかこうにか頑張って学習し,要求を満たそうとします.
結果としてモデルは複雑な挙動を示すようになり,過学習を引き起こし易くなります.
ラベルスムージングはこの問題に対処するための手法で,やっていることは非常にシンプルです.
図1の右側に示すように,まず最初に正解ラベルの目標値をハイパーパラメータの設定に従って減らします.
次に,減らした分の確率値を他のラベルに均等に割り振ります.
図1の例では,ハイパーパラメータを \(\epsilon = 0.1\) とした場合,\(L_3\) の目標値は \(1 - \epsilon = 0.9\) となり,他のラベルの目標値は \(\epsilon/3\) になります.
非常にシンプルながら,この操作によってモデルは学習データに対して極端な推論をしなくなり,過学習を抑制することが可能です.
2. 実験結果
次節以降では,いつも通り実装の紹介をしながら実験結果をお見せします.
コード紹介記事の方針として記事単体で全処理が分かるように書いており,かつ,今回に関しては今までのコードを1行だけ変えれば良いので,結果を先にお見せしたいと思います.
図2は,ラベルスムージング設定値毎のValidation Lossと認識率の推移を示しています.
![図2: 認識性能比較結果](https://takayama-rado.com/images/articles/slr_tutorial_simple_transformer_ls/evaluation_result.png)
横軸は学習・評価ループの繰り返し数 (Epoch) を示します.
縦軸はそれぞれの評価指標を示します.
各線の色と実験条件の関係は次のとおりです.
- 青線 (Default): Pre-LN構成のTransformer
- 橙線 (+ LS=0.1): \(\epsilon = 0.1\) (正解ラベルの確率:0.9, 他のラベルの合計確率: 0.1)
- 緑線 (+ LS=0.3): \(\epsilon = 0.3\) (正解ラベルの確率:0.7, 他のラベルの合計確率: 0.3)
- 赤線 (+ LS=0.5): \(\epsilon = 0.5\) (正解ラベルの確率:0.5, 他のラベルの合計確率: 0.5)
デフォルトのモデルには,第九回の記事で紹介した,Pre-LN構成のTransformerモデルを用います.
青線の結果から分かるように,ラベルスムージングを使わない場合は 20 epoch 以降で過学習気味になっており,認識性能も伸び悩んでいます.
一方,ラベルスムージングを用いた場合はロスの挙動は安定しており,認識性能も向上していることが分かります.
今回は \(\epsilon = 0.5\) とかなり強めにラベルスムージングを適用した場合でも認識性能が向上しました.
最適なパラメータはデータセットやタスク毎に異なりますので,実際に使用する場合はある程度の試行錯誤が必要です.
なお,今回の実験では話を簡単にするために,実験条件以外のパラメータは固定にし,乱数の制御もしていません.
必ずしも同様の結果になるわけではないので,ご了承ください.
3. ラベルスムージングの実装
いつもであれば,処理の頭から実装の解説を始めるのですが,今回は第九回のコードを1行変えれば良いだけですので,ここで変更箇所を示します.
(もちろん,本記事を最初に見ていただいた方向けに,次節以降で全体コードの紹介もします)
ラベルスムージングはPyTorchの CrossEntropyLoss()
クラスに標準で実装されており,下記のように変更することで有効にできます.
# Instantiation without label smoothing.
loss_fn = nn.CrossEntropyLoss(reduction="mean")
# Instantiation with label smoothing.
label_smoothing = 0.1
loss_fn = nn.CrossEntropyLoss(reduction="mean", label_smoothing=label_smoothing)
loss_fn
はクロスエントロピー誤差を計算するためのインスタンスです.
インスタンス化時に label_smoothing
引数に値を渡して,後はいつも通りに使用すればラベルスムージングが有効になった学習が実行されます.
4. 前準備
4.1 データセットのダウンロード
ここからは実装方法の説明をしていきます.
まずは,前準備としてGoogle Colabにデータセットをアップロードします.
ここの工程はこれまでの記事と同じですので,既に行ったことのある方は第4.3項まで飛ばしていただいて構いません.
まず最初に,データセットの格納先からデータをダウンロードし,ご自分のGoogle driveへアップロードしてください.
次のコードでGoogle driveをColabへマウントします.
Google Driveのマウント方法については,補足記事にも記載してあります.
1 2 3 |
|
ドライブ内のファイルを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 |
|
['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.1.zip -O master.zip
--2024-01-21 11:01:47-- https://github.com/takayama-rado/trado_samples/archive/master.zip
Resolving github.com (github.com)... 140.82.112.3
...
2024-01-21 11:01:51 (19.4 MB/s) - ‘master.zip’ saved [75710869]
ダウンロードしたリポジトリを解凍します.
!unzip -o master.zip -d master
Archive: master.zip
641b06a0ca7f5430a945a53b4825e22b5f3b8eb6
creating: master/trado_samples-main/
inflating: master/trado_samples-main/.gitignore
...
モジュールのディレクトリをカレントディレクトリに移動します.
!mv master/trado_samples-main/src/modules_gislr .
他のファイルは不要なので削除します.
!rm -rf master master.zip gislr_top10.zip
!ls
dataset_top10 drive modules_gislr sample_data
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 |
|
【コード解説】
- 標準モジュール
- copy: データコピーライブラリ.Transformerブロック内でEncoder層をコピーするために使用します.
- json: JSONファイル制御ライブラリ.辞書ファイルのロードに使用します.
- math: 数学計算処理ライブラリ
- sys: Pythonインタプリタの制御ライブラリ.
今回はローカルモジュールに対してパスを通すために使用します.
- functools: 関数オブジェクトを操作するためのライブラリ.
今回はDataLoaderクラスに渡すパディング関数に対して設定値をセットするために使用します.
- inspect.signature: オブジェクトの情報取得ライブラリ.
- pathlib.Path: オブジェクト指向のファイルシステム機能.
主にファイルアクセスに使います.osモジュールを使っても同様の処理は可能です.
高山の好みでこちらのモジュールを使っています(^^;).
- typing: 関数などに型アノテーションを行う機能.
ここでは型を忘れやすい関数に付けていますが,本来は全てアノテーションをした方が良いでしょう(^^;).
- 3rdパーティモジュール
- numpy: 行列演算ライブラリ
- torch: ニューラルネットワークライブラリ
- torchvision: PyTorchと親和性が高い画像処理ライブラリ.
今回はDatasetクラスに与える前処理をパッケージするために用います.
- ローカルモジュール: sys.pathにパスを追加することでロード可能
- dataset: データセット操作用モジュール
- defines: 各部位の追跡点,追跡点間の接続関係,およびそれらへのアクセス処理を
定義したモジュール
- layers: ニューラルネットワークのモデルやレイヤモジュール
- transforms: 入出力変換処理モジュール
- train_functions: 学習・評価処理モジュール
5. 認識モデルの動作確認
今回は,第九回の記事で紹介した,Pre-LN構成のTransformerモデルをそのまま用いて実験を行います.
ここではモデルの推論動作が正常に動くかだけ確かめます.
次のコードでデータセットからHDF5ファイルとJSONファイルのパスを読み込みます.
1 2 3 4 5 6 7 8 |
|
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 |
|
次のコードで前処理を定義します.
今回は,以前に説明した追跡点の選定と,追跡点の正規化を前処理として適用して実験を行います.
1 2 3 4 5 6 7 8 9 10 |
|
次のコードで,前処理を適用したHDF5DatasetとDataLoaderをインスタンス化し,データを取り出します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
torch.Size([2, 2, 60, 130])
次のコードでモデルをインスタンス化して,動作チェックをします.
追跡点抽出の結果,入力追跡点数は130で,各追跡点はXY座標値を持っていますので,入力次元数は260になります.
出力次元数は単語数なので10になります.
また,Transformer層の入力次元数は64に設定し,PFFN内部の拡張次元数は256に設定しています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
TransformerEnISLR(
(linear): Linear(in_features=260, out_features=64, bias=True)
(activation): ReLU()
(tr_encoder): TransformerEncoder(
(pos_encoder): PositionalEncoding(
(dropout): Dropout(p=0.1, inplace=False)
)
(layers): ModuleList(
(0-1): 2 x TransformerEncoderLayer(
(self_attn): MultiheadAttention(
(w_key): Linear(in_features=64, out_features=64, bias=True)
(w_value): Linear(in_features=64, out_features=64, bias=True)
(w_query): Linear(in_features=64, out_features=64, bias=True)
(w_out): Linear(in_features=64, out_features=64, bias=True)
(dropout_attn): Dropout(p=0.1, inplace=False)
)
(ffw): PositionwiseFeedForward(
(w_1): Linear(in_features=64, out_features=256, bias=True)
(w_2): Linear(in_features=256, out_features=64, bias=True)
(dropout): Dropout(p=0.1, inplace=False)
(activation): ReLU()
)
(dropout): Dropout(p=0.1, inplace=False)
(norm1): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
(norm2): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
)
)
(norm): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
)
(head): GPoolRecognitionHead(
(head): Linear(in_features=64, out_features=10, bias=True)
)
)
torch.Size([2, 10])
(2, 2, 60, 60) (2, 2, 60, 60)
6. 学習と評価の実行
6.1 共通設定
では,実際に学習・評価を行います.
まずは,実験全体で共通して用いる設定値を次のコードで実装します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Using 2 cores for data loading.
Using cuda for computation.
次のコードで学習・バリデーション・評価処理それぞれのためのDataLoaderクラスを作成します.
1 2 3 4 5 6 7 8 9 10 11 |
|
6.2 学習・評価の実行
次のコードでモデルをインスタンス化します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
TransformerEnISLR(
(linear): Linear(in_features=260, out_features=64, bias=True)
(activation): ReLU()
(tr_encoder): TransformerEncoder(
(pos_encoder): PositionalEncoding(
(dropout): Dropout(p=0.1, inplace=False)
)
(layers): ModuleList(
(0-1): 2 x TransformerEncoderLayer(
(self_attn): MultiheadAttention(
(w_key): Linear(in_features=64, out_features=64, bias=True)
(w_value): Linear(in_features=64, out_features=64, bias=True)
(w_query): Linear(in_features=64, out_features=64, bias=True)
(w_out): Linear(in_features=64, out_features=64, bias=True)
(dropout_attn): Dropout(p=0.1, inplace=False)
)
(ffw): PositionwiseFeedForward(
(w_1): Linear(in_features=64, out_features=256, bias=True)
(w_2): Linear(in_features=256, out_features=64, bias=True)
(dropout): Dropout(p=0.1, inplace=False)
(activation): ReLU()
)
(dropout): Dropout(p=0.1, inplace=False)
(norm1): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
(norm2): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
)
)
(norm): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
)
(head): GPoolRecognitionHead(
(head): Linear(in_features=64, out_features=10, bias=True)
)
)
次のコードで学習・評価処理を行います.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Start training.
--------------------------------------------------------------------------------
Epoch 1
Start training.
/usr/lib/python3.10/multiprocessing/popen_fork.py:66: RuntimeWarning: os.fork() was called. os.fork() is incompatible with multithreaded code, and JAX is multithreaded, so this will likely lead to a deadlock.
self.pid = os.fork()
loss:5.287299 [ 0/ 3881]
loss:1.960738 [ 3200/ 3881]
Done. Time:3.9779580539999984
Training performance:
Avg loss:2.319880
Start validation.
Done. Time:0.23274805799999854
Validation performance:
Avg loss:2.077236
Start evaluation.
Done. Time:1.3301631420000035
Test performance:
Accuracy:23.0%
--------------------------------------------------------------------------------
...
--------------------------------------------------------------------------------
Epoch 50
Start training.
loss:0.167262 [ 0/ 3881]
loss:0.119092 [ 3200/ 3881]
Done. Time:3.317633838000006
Training performance:
Avg loss:0.192512
Start validation.
Done. Time:0.2518004840000003
Validation performance:
Avg loss:1.089958
Start evaluation.
Done. Time:1.121303241000021
Test performance:
Accuracy:78.0%
Minimum validation loss:0.8578660488128662 at 20 epoch.
Maximum accuracy:78.0 at 50 epoch.
以後,同様の処理をラベルスムージングの設定毎に繰り返します.
コード構成は同じですので,ここでは説明を割愛させていただきます.
また,この後グラフ等の描画も行っておりますが,本記事の主要点ではないため説明を割愛させていただきます.
なお,冒頭のメッセージは,マルチスレッドを fork
という方法で立ち上げた場合に出る警告のようです.
内部動作に寄るもので,ユーザ側からの対策方法は分かっていませんが,現象にご興味がある方はこちらのリンクをご参照ください.
今回はラベルスムージングを用いた認識モデルの改善手法を紹介しましたが,如何でしたでしょうか?
孤立手話単語認識のようなオーソドックスなタスクの場合は,PyTorchの標準実装が使えるので便利ですね(^^).
連続手話単語認識に向いたラベルスムージング [Kim'18] も提案されていますので,機会があれば別記事で紹介したいと思います.
今回紹介した話が,これから手話認識を勉強してみようとお考えの方に何か参考になれば幸いです.
- [Kim'18]: S. Kim, et al., "Improved training for online end-to-end speech recognition systems," Proc. of the INTERSPEECH, available here, 2018.