はじめに
この記事は、この記事誰得? 私しか得しないニッチな技術で記事投稿! キャンペーンへの参加記事です。
世はまさに大規模言語モデル全盛期。
Transformer、拡散モデル、Mixture of Expertsなど、華やかなアーキテクチャが次々と登場しています。
そんな時代に、私はあえて声を大にして言いたい。
皆さん、全結合ニューラルネットワークの隠れ層を、親の顔より見つめたことはありますか?
本記事は、とある特殊な環境で動作するセンサーから得られる、時間変化するバックグラウンド成分を推定するために、シンプルな多層パーセプトロン、いわゆるMLPをひたすらチューニングした記録です。
派手な新規アーキテクチャは登場しません。
あるのは、隠れ層の数、ユニット数、活性化関数、重みの初期値、オプティマイザと地道に向き合い続けた結果だけです。
最終的に得られた知見は、かなりデータ依存です。
おそらく、ほとんどの人にはそのまま役立ちません。
しかし、「特定のセンサーデータに対して、小規模なMLPを本気で調整する」という状況に限れば、地球上のどこかに1人くらいは同じ沼にはまっている人がいるかもしれません。
その1人に向けて、この記事を書きます。
今回の相手は、時間変化するバックグラウンド
今回扱ったのは、画像でも文章でもありません。
とある特殊な環境で稼働するセンサーから得られる、時間変化するバックグラウンドデータです。
このバックグラウンドは一定ではなく、複数の環境パラメータに依存しながら複雑に変化します。
観測データを単純化すると、次のような関係です。
観測データ = 見たい信号 + バックグラウンド
本来見たい信号を取り出すためには、バックグラウンドをできるだけ正確に推定し、観測データから差し引く必要があります。
従来は、経験的な関係式を用いてバックグラウンドをモデル化していました。
しかし、観測条件や環境が変わるたびにパラメータを調整する必要があり、運用がかなり属人的になっていました。
そこで、環境パラメータを入力し、バックグラウンド量を出力する回帰モデルを機械学習で構築することにしました。
ただし、今回のデータには次の制約があります。
- データ量がそれほど多くない
- 特定の環境に強く依存したドメイン特化データである
- モデルの挙動をある程度追跡できる必要がある
- 過学習を起こすと、実際のデータ解析に直接影響する
複雑なモデルを導入すればよいとは限りません。
そこで、構造が単純で挙動も比較的確認しやすいMLPを採用しました。
なぜ、わざわざMLPを細かく調整したのか
現在であれば、適当な機械学習ライブラリを使って、隠れ層を数層追加し、オプティマイザにAdamを指定すれば、それらしいモデルはすぐに作れます。
しかし、今回は入力データも学習データ数も限られています。
モデルを大きくすれば、学習データに対する損失は簡単に下がります。
一方で、未知のデータに対する予測性能が改善するとは限りません。
そこで、次の要素を一つずつ変更しながら比較しました。
- 隠れ層の数
- 各隠れ層のユニット数
- 活性化関数
- 重みの初期値
- オプティマイザ
今振り返ると、かなり古典的なハイパーパラメータ探索です。
しかし、これを実際のセンサーデータに対して行うと、教科書で見た説明がかなり具体的な挙動として現れました。
誰得検証1:隠れ層は1層か、2層か
最初に調べたのは、隠れ層の数です。
「層を増やせば複雑な関係を表現できるのだから、深くすればよいのでは」と考えたくなります。
しかし、今回のような小規模データでは、ネットワークを深くするほど過学習の危険も増えます。
そこで、まずは隠れ層1層と2層を比較しました。
隠れ層1層
隠れ層1層のモデルでは、バックグラウンドの大まかな時間変化は捉えられました。
一方で、複数の環境パラメータが組み合わさったときに現れる細かな変化を十分に表現できませんでした。
学習データに対する損失も、ある程度下がったところで頭打ちになります。
今回のデータに対しては、やや表現力が不足していると判断しました。
隠れ層2層
隠れ層を2層にすると、1層の場合よりも細かな変動を捉えられるようになりました。
一方で、ユニット数を増やしすぎると、学習データにだけ存在する揺らぎまで再現し始めます。
つまり、表現力は十分ですが、設定を間違えるとすぐに過学習します。
この結果から、以降は隠れ層2層を基本構成として、各層のユニット数を調整することにしました。
誰得検証2:ユニット数64、200、400の戦い
隠れ層を2層に固定し、各層のユニット数を変更して比較しました。
今回試した代表的な構成は、64、200、400ユニットです。
| 各隠れ層のユニット数 | 学習時の挙動 | 評価 |
|---|---|---|
| 64 | 大まかな傾向は捉えるが、細かな変動を十分に再現できない | 表現力不足 |
| 200 | 学習データと評価データの両方で比較的安定した結果 | 最もバランスがよい |
| 400 | 学習データには非常によく適合するが、未知データで不自然な予測が増える | 過学習傾向 |
64ユニット:全体的に予測がぼんやりする
64ユニットでは、バックグラウンドの大きな増減は追跡できました。
しかし、変化の細部が平滑化され、実際のデータよりも予測結果が全体的にぼんやりします。
学習損失と検証損失がともに高めだったため、過学習というよりは、モデルの表現力が不足している状態でした。
200ユニット:最も扱いやすかった
200ユニットでは、全体的な時間変化と細かな変動の両方を比較的よく再現できました。
学習データに対する誤差を十分に下げながら、評価データに対する誤差も大きく悪化しませんでした。
今回のデータに対しては、モデルの表現力と汎化性能のバランスが最もよい構成でした。
400ユニット:学習データには強いが、未知データで暴れる
400ユニットにすると、学習データに対する誤差は大きく下がりました。
損失関数だけを見れば、非常に優秀なモデルに見えます。
しかし、評価データに適用すると、実際には存在しない鋭いピークや不自然な変動を生成することがありました。
学習データ内の細かな揺らぎまで記憶してしまったと考えられます。
学習データに対する精度が最も高いモデルが、最もよいモデルとは限らない。
教科書では何度も見る話ですが、実際のデータで不自然なピークが生成される様子を見ると、かなり実感を伴って理解できます。
誰得検証3:活性化関数の聖戦
次に、活性化関数を比較しました。
試したのは、Sigmoid、tanh、ReLUです。
Sigmoid:学習が途中から進みにくい
Sigmoid関数は、出力が0から1の範囲に収まるため、直感的には扱いやすい活性化関数です。
しかし、今回の構成では、学習の途中から損失の減少がかなり緩やかになりました。
入力の絶対値が大きい領域では勾配が小さくなりやすく、前段の層まで十分な勾配が伝わりにくかった可能性があります。
学習率を変更しても改善は限定的だったため、今回の候補からは外しました。
tanh:Sigmoidよりはよいが、収束が遅い
tanhは出力がマイナスからプラスの範囲を取り、Sigmoidよりもゼロ中心に近い活性化関数です。
Sigmoidと比較すると学習は進みやすくなりました。
ただし、今回のデータでは、ReLUと比較して損失の低下が遅く、初期値による結果のばらつきも比較的大きくなりました。
モデルとして使えないわけではありませんが、積極的に採用する理由は見つかりませんでした。
ReLU:最も安定して収束した
今回のデータでは、ReLUが最も安定して学習しました。
学習初期から損失が比較的速く低下し、Sigmoidやtanhで見られたような収束の停滞も少なくなりました。
もちろん、ReLUにもニューロンの出力が常にゼロになる、いわゆるdying ReLUの問題があります。
それでも、今回試した範囲では、Sigmoidやtanhよりも扱いやすい結果となりました。
最終的に、活性化関数にはReLUを採用しました。
誰得検証4:初期値ガチャとの戦い
ネットワーク構造を次のように固定した後も、問題が残っていました。
- 隠れ層:2層
- 各層のユニット数:200
- 活性化関数:ReLU
同じ条件で学習しているはずなのに、乱数シードによって最終的な予測精度が変わります。
うまく学習できる場合もあれば、損失の下がり方が鈍く、精度が十分に伸びない場合もありました。
いわゆる初期値ガチャです。
適当なガウス分布で初期化していた
当初は、重みを平均0のガウス分布から生成していました。
しかし、重みの分散を適切に設定していなかったため、層を進むにつれて信号の分散が変化し、学習初期の勾配が不安定になっていた可能性があります。
ReLUを使用しているにもかかわらず、初期値について深く考えていませんでした。
そこで、重みの初期化にHeの初期値を導入しました。
Heの初期値を導入する
ReLUを使用する場合、入力ユニット数を ($n_{\mathrm{in}}$) とすると、重みをおおむね次の分散を持つ分布から初期化します。
$$
\mathrm{Var}(w) = \frac{2}{n_{\mathrm{in}}}
$$
実装によっては、標準偏差を次のように設定します。
$$
\sigma = \sqrt{\frac{2}{n_{\mathrm{in}}}}
$$
Heの初期値を導入すると、学習初期に損失が急激に不安定になるケースが減り、異なる乱数シードでも似た傾向で収束するようになりました。
予測精度そのものが突然何倍にも改善したわけではありません。
それよりも大きかったのは、学習結果の再現性が改善したことです。
センサーデータの解析では、特定の乱数シードで偶然よい結果が得られるモデルより、複数回学習しても安定した性能を示すモデルの方が安心して使えます。
初期値は、単なる実装上の細部ではありませんでした。
誰得検証5:SGD、Momentum、AdaGradを比較する
最後に、オプティマイザを比較しました。
試したのは次の3種類です。
- SGD
- Momentum
- AdaGrad
現在であればAdamやAdamWを使うケースが多いと思います。
しかし、この検証を行った当時は、更新則の違いを明確に比較するため、より基本的なオプティマイザを選びました。
SGD:学習率の調整が難しい
SGDでは、すべてのパラメータを同じ学習率で更新します。
設定が単純で挙動も理解しやすい一方、今回のデータでは学習率の調整が難しく感じられました。
学習率を大きくすると損失が振動し、小さくすると収束に時間がかかります。
損失関数の谷に入った後も、最小値付近を行き来するような挙動が見られました。
Momentum:速いが、振動する場合がある
Momentumを導入すると、SGDよりも学習が速く進みました。
一方で、設定によっては損失が大きく振動しました。
勾配の方向に慣性を持たせるため、細長い谷では効率よく進めますが、今回の損失関数では最適値付近を通り過ぎる場合があったと考えられます。
学習率とMomentum係数を適切に調整すれば改善できた可能性はあります。
ただし、今回の探索範囲では、安定性の面でAdaGradの方が扱いやすい結果になりました。
AdaGrad:今回の特徴量構成と相性がよかった
AdaGradは、パラメータごとに過去の勾配情報を蓄積し、更新量を調整します。
頻繁に更新されるパラメータでは学習率が徐々に小さくなり、更新頻度の低いパラメータでは比較的大きな更新量が保たれます。
今回使用した入力特徴量には、頻繁に変化するものと、特定の条件でのみ大きく変化するものが混在していました。
そのため、パラメータごとに実効的な学習率を変えるAdaGradが、今回のデータと相性がよかった可能性があります。
実際、試した範囲では損失の低下が安定しており、比較的少ないエポック数で十分な精度に到達しました。
もちろん、AdaGradには学習が進むほど学習率が小さくなりすぎるという弱点があります。
それでも、今回のような小規模データセットと学習回数の範囲では、大きな問題にはなりませんでした。
最終的に完成した、オレ専用バックグラウンド推定モデル
数え切れないほどの学習を繰り返した結果、今回のセンサーデータに対して最も安定した構成は次のようになりました。
| 項目 | 採用した設定 |
|---|---|
| ネットワーク構造 | 全結合ニューラルネットワーク |
| 隠れ層 | 2層 |
| 各隠れ層のユニット数 | 200 |
| 活性化関数 | ReLU |
| 重みの初期値 | Heの初期値 |
| 最適化手法 | AdaGrad |
最新の巨大モデルと比較すれば、非常に古風で単純な構成です。
しかし、この構成に至るまでの過程で、次のことを実際のデータを通して理解できました。
- モデルを大きくすれば精度が上がるとは限らない
- 学習損失だけではモデルを評価できない
- 活性化関数と重みの初期値はセットで考える必要がある
- 乱数シードを変えた複数回の評価が重要である
- オプティマイザとデータの相性は無視できない
- 古典的なMLPでも、適切に調整すれば十分に使える
特に大きかったのは、単に精度の高いモデルを得られたことではありません。
なぜ、その構成が今回のデータに対して安定するのかを、自分なりに説明できるようになったことです。
本当に「200ユニットが最強」なのか
もちろん、この記事の結果から「MLPは隠れ層2層、200ユニット、ReLU、He初期値、AdaGradにすべき」と一般化することはできません。
200ユニットが適切だったのは、今回の入力特徴量、データ数、バックグラウンドの複雑さ、評価方法が組み合わさった結果です。
データセットが変われば、最適な構成も変わります。
また、探索したハイパーパラメータの範囲も限定的です。
より細かな探索を行えば、180ユニットや256ユニットの方がよかった可能性もあります。
AdamやAdamW、学習率スケジューラ、Batch Normalization、Dropoutなどを導入すれば、さらに改善する可能性もあります。
したがって、この記事で共有したいのは、最終的な数値そのものではありません。
重要なのは、小規模でドメイン特化したデータだからこそ、モデルの各要素を一つずつ比較する価値があるという点です。
おわりに
今回構築したのは、とある特殊なセンサーのバックグラウンドを推定するためだけの、小さなMLPです。
他のデータにそのまま適用できる保証はありません。
最先端のモデルでもありません。
おそらく、多くの人にとっては「Adamでよくない?」で終わる話です。
それでも、隠れ層の数を変え、ユニット数を増減させ、Sigmoidが止まる様子を眺め、初期値ガチャに苦しみ、最後にHeの初期値とAdaGradへたどり着いた経験は、機械学習の基礎を理解するうえで非常に役立ちました。
他の誰にも必要とされない、極めてニッチな知見かもしれません。
しかし、地球上のどこかで、
「特殊なセンサーのバックグラウンドを、小規模なMLPでどうにか推定したい」
と考えている奇特な方がいれば、この記事がわずかでも参考になれば幸いです。
そして、もしモデルの学習が乱数シードによって安定しないのであれば、ネットワークを複雑にする前に、重みの初期値を一度見直してみてください。
初期値は、思っている以上にモデルを見ています。