2025-12-23

ラビット・チャレンジ - Stage 3. 深層学習 前編 (Day 3)

提出したレポートです。

絶対書きすぎですが、行間を埋めたくなるので仕方ない。


Rabbit Challenge - Stage 3. 深層学習 前編 (Day 3)

1. 再帰型ニューラルネットワークの概念

1.1. RNN 全体像

再帰型ニューラルネットワーク(Recurrent Neural Network : RNN)は、時系列データを扱うことを目的として設計されたニューラルネットワークである。

通常の多層パーセプトロンや畳み込みニューラルネットワークでは、入力データ同士が独立であること(i.i.d.)を暗黙的に仮定している。

一方で、自然言語や音声、時系列信号などでは、過去の入力が現在の出力に影響を与えるという性質が本質的である。

RNN はこの性質をモデル化するために、過去の情報を内部状態として保持し、それを次の計算に再利用する 再帰構造 を導入している。

1.1.1. RNN とは

RNN とは、各時刻において入力と同時に直前の中間状態(隠れ状態)を入力として受け取るニューラルネットワークである。

時刻 $t$ における隠れ状態 $\boldsymbol{h}_t$ は、現在の入力 $\boldsymbol{x}_t$ と直前の隠れ状態 $\boldsymbol{h}_{t-1}$ を用いて計算される。

$$ \boldsymbol{h}_t = f\left( W_x \boldsymbol{x}_t + W_h \boldsymbol{h}_{t-1} + \boldsymbol{b} \right) $$

ここで、

  • $W_x$:入力に対する重み
  • $W_h$:再帰(隠れ状態)に対する重み
  • $\boldsymbol{b}$:バイアス
  • $f(\cdot)$:活性化関数

である。

このように、RNN は「現在」と「過去」を結びつけることで、 系列全体の文脈を内部状態として表現する。

1.1.2. 時系列データ

時系列データとは、時間的な順序を持ち、順序を入れ替えることができないデータである。

代表例として、

  • 自然言語(単語列)
  • 音声信号
  • 株価・センサーデータ

などが挙げられる。

これらのデータでは、単一の入力だけでなく「どの順番で現れたか」が意味を持つため、入力同士を独立に扱うモデルでは十分な表現ができない。

RNN は、この「順序依存性」を内部状態に埋め込むことで、時系列データを扱うことを可能にしている。

1.1.3. RNN について

RNN の特徴は、同一の重みを全時刻で共有する点にある。

時間方向に RNN を展開すると、「重みを共有した非常に深いネットワーク」として解釈できる。

この重み共有により、

  • パラメータ数を抑えられる
  • 任意長の系列を扱える

という利点が得られる一方で、後述する 勾配消失・勾配爆発問題 を引き起こす原因ともなる。

1.2. BPTT(応用学習)

1.2.1. BPTT とは

BPTT(Backpropagation Through Time)とは、RNN に対して誤差逆伝播法を適用するための手法である。

RNN は再帰構造を持つため、そのままでは通常の誤差逆伝播を適用できない。

そこで、時間方向にネットワークを展開し、各時刻を一つの層とみなして誤差逆伝播を行う。

この「時間方向への展開」を考慮した誤差逆伝播が BPTT である。

1.2.2. BPTT の数学的記述

RNN を時間方向に展開すると、損失関数 $L$ は各時刻の損失 $L_t$ の和として表される。

$$ L = \sum_t L_t $$

再帰重み $W_h$ に対する勾配は、各時刻における勾配が連鎖的に伝播したものの総和となる。

このとき、活性化関数の微分や重み行列の積が時間方向に繰り返し現れるため、勾配が指数的に減衰または増大する可能性が生じる。

1.2.3. BPTT の全体像

BPTT によって、RNN は理論上は長期依存関係を学習できる。しかし実際には、

  • 勾配消失問題
  • 勾配爆発問題

により、長い系列に対する学習は困難である。

この問題を構造的に解決するために提案されたモデルが、次章で扱う LSTM である。

実装演習

まず、3_1_simple_RNN_after.ipynb の実行結果が、以下の通り。

stage_3_3_01.png

学習初期には損失が緩やかに減少し、その後 3,000〜4,000 iteration 付近で急激に損失が低下していることが確認できる。

これは、モデルがデータの基本的な構造を段階的に学習した後、判別に有効な特徴表現を獲得できたためと考えられる。

また、学習後半では損失がほぼ 0 に近い値で安定しており、発散や大きな振動は見られない。このことから、本設定では勾配爆発や学習率過大による不安定化は生じておらず、学習が安定して進行しているといえる。

以上より、これは適切なハイパーパラメータ設定により、単純な全結合ネットワークであっても十分に収束することを示しており、以降の try 実験における比較の基準(ベースライン)として妥当な結果である。

[try] weight_init_std や learning_rate, hidden_layer_size を変更してみよう

stage_3_3_02.png

まず、学習率を過大に設定した条件(lr too high)では、学習初期に損失が急激に増大し、その後も高い値で不安定に推移している。これは勾配更新が過剰となり、最適解付近を行き過ぎることで学習が収束しなくなる典型的な例であり、学習率が学習の安定性に強く影響することを示している。

次に、重み初期値の分散を大きく設定した条件(std too big)では、学習初期に非常に大きな損失が発生し、その後も振動を伴いながらゆっくりと減少している。この挙動は、初期段階で活性値や勾配が過大になり、学習が不安定化した結果と考えられる。

一方、適切な分散で初期化されたベースラインでは、損失が滑らかに減少しており、初期化の重要性が確認できる。

また、隠れ層のユニット数が極端に小さい場合(small hidden)では、損失は比較的早期に収束するものの、最終的な損失はベースラインより高く、表現能力不足による性能低下が示唆される。

一方で、隠れ層を過度に大きくした条件(large hidden)では、学習初期の収束はやや遅くなるが、最終的な損失は低く抑えられており、モデル容量の増加が表現力向上に寄与していることが分かる。

以上より、学習率・重み初期化・隠れ層サイズはいずれも学習の安定性および収束性能に大きな影響を与える要素であり、これらを適切に設定することが深層学習モデルの学習を成功させるために不可欠であることが、本実験結果から確認できる。

[try] 重みの初期化方法を変更してみよう

stage_3_3_03.png

分散の大きい標準正規分布による初期化(std_bad)では、初期損失が非常に大きく、学習中の損失変動も大きいことが確認できる。これは、初期重みが過度に大きいため、順伝播・逆伝播の段階で活性値および勾配が不安定になるためである。

一方、Xavier 初期化および He 初期化では、初期から損失が比較的低く抑えられ、学習も安定して進行している。特に ReLU を用いた場合には He 初期化が理論的に適しており、勾配の分散が層間で適切に保たれていると考えられる。

この結果から、初期化手法は最終性能だけでなく、学習初期の安定性や収束速度に大きく影響することが分かる。

[try] 中間層の活性化関数を変更してみよう

stage_3_3_04.png

ReLU を高い学習率で用いた場合、学習初期に損失が急激に増大するスパイクが観測され、その後も損失が高止まりする傾向が見られる。これは、ReLU が正の領域で勾配が一定であるため、学習率が大きいと重み更新が過剰になり、勾配爆発を引き起こしやすいためである。

一方、tanh を用いた場合は損失が滑らかに減少し、安定した学習が行われている。tanh は出力が $[-1,1]$ に制限されるため、勾配の大きさも抑制され、学習が安定しやすい。

この結果から、活性化関数の特性と学習率の組み合わせが学習安定性に大きく影響することが確認できる。

確認テスト

RNN のネットワークには大きくわけて 3 つの重みがある。1 つは入力から現在の中間層を定義する際にかけられる重み、1 つは中間層から出力を定義する際にかけられる重みである。残り 1 つの重みについて説明せよ

RNN の重みは大きく 3 つある。

  1. 入力 → 中間層(隠れ状態) を計算する重み(例:$W_x$)
  2. 中間層 → 出力 を計算する重み(例:$W_y$)
  3. 中間層 → 次時刻の中間層(再帰) を計算する重み(例:$W_h$ または $U$)

設問の「残り 1 つの重み」とは、③の 中間層から次の時刻の中間層へつながる再帰重みである。

下図の $y_1$ を $x, z_0, z_1, W_{in}, W, W_{out}$ を用いて数式で表せ

図に示された RNN において、出力 $y_1$ を $x, z_0, z_1, W_{in}, W, W_{out}$ を用いて数式で表す。

まず、時刻 $0$ における中間層(隠れ状態)$z_0$ は、初期状態として与えられているものとする。

時刻 $1$ における中間層 $z_1$ は、入力 $x_1$ と直前の中間層 $z_0$ を用いて計算される。

中間層にはシグモイド関数 $g(x)$ を作用させるため、$z_1$ は次式で表される。

$$ z_1 = g(W_{in} x_1 + W z_0 + b) $$

ここで $b$ はバイアスである。

次に、出力 $y_1$ は中間層 $z_1$ から出力層への重み $W_{out}$ を用いて次式で表される。

$$ y_1 = W_{out} z_1 + c $$

ここで $c$ は出力層のバイアスである。

以上より、$y_1$ は

$$ y_1 = W_{out} \cdot g(W_{in} x_1 + W z_0 + b) + c $$

と表される。

参考図書 / 関連記事

  • ゼロから作る Deep Learning ② - 自然言語処理編(斎藤 康毅 著)
    • p.188 - 190

Truncated BPTT(Truncated Backpropagation Through Time)は、RNN における誤差逆伝播を時間方向に無制限に行うのではなく、一定の長さで打ち切って行う手法 である。

通常の BPTT では、系列全体に対して誤差を逆伝播させるため、系列が長くなるほど計算量やメモリ使用量が増大し、学習が現実的でなくなるという問題がある。また、長い系列では勾配消失や勾配爆発が発生しやすい。

Truncated BPTT では、RNN の順伝播は連続した時系列として行う一方で、逆伝播の計算は一定ステップごとに区切って実行する。これにより、過去すべての時刻に対して誤差を伝播させる必要がなくなり、計算コストとメモリ使用量を大幅に削減できる。

ただし、逆伝播を打ち切るため、非常に長期の依存関係を直接学習することは困難となる。そのため、Truncated BPTT は計算効率と表現能力のトレードオフを考慮した、実用的な RNN 学習手法として位置づけられる。


2. LSTM

LSTM(Long Short-Term Memory)は、RNN における勾配消失問題を解決することを目的として提案されたモデルである。

LSTM では、情報の通り道を明示的に分離し、勾配が長時間にわたって伝播しやすい構造を持つ。

2.1. CEC

CEC(Constant Error Carousel)は、LSTM における セル状態 に相当する。

セル状態は、加算を中心とした更新構造を持ち、誤差が減衰しにくい経路として機能する。

この構造により、勾配が時間方向に安定して伝播し、長期依存関係の学習が可能となる。

2.2. 入力ゲートと出力ゲート

入力ゲートは、新しい情報をセル状態にどの程度書き込むかを制御する。

出力ゲートは、セル状態の情報をどの程度外部に出力するかを制御する。

これらのゲートはシグモイド関数によって $0 \sim 1$ の連続値を出力し、情報の流れを滑らかに制御する。

2.3. 忘却ゲート

忘却ゲートは、セル状態に保持されている過去情報をどの程度残すかを制御する。

このゲートの導入により、

  • 不要になった情報を明示的に捨てる
  • 長期的に重要な情報のみを保持する

ことが可能となり、LSTM の表現力と安定性が大きく向上した。

2.4. 覗き穴結合

覗き穴結合(Peephole Connection)は、セル状態を直接ゲート計算に利用する拡張構造である。

これにより、セル状態の値そのものを参照した、より精密なゲート制御が可能となる。

実装演習

なし。

確認テスト

以下の文章をLSTMに入力し空欄に当てはまる単語を予測したいとする。文中の「とても」という言葉は空欄の予測においてなくなっても影響を及ぼさないと考えられる。このような場合、どのゲートが作用すると考えられるか

文中の「とても」という語は、空欄に入る語の予測において重要な意味情報を持たず、予測に影響を与えないと考えられている。

このような場合、LSTM では 忘却ゲート が主に作用すると考えられる。

忘却ゲートは、セル状態に保持されている過去の情報のうち、どの情報を残し、どの情報を破棄するかを制御する役割を持つ。

「とても」は強調を表す副詞であり、文全体の意味や空欄に入る語(例:食事内容など)を決定するうえで本質的な情報ではないため、忘却ゲートによって影響が小さくなる、あるいはセル状態から除去されると考えられる。

したがって、本問のように「予測に不要な語の影響を抑える」状況では、忘却ゲートが作用している と説明できる。

参考図書 / 関連記事

  • ゼロから作る Deep Learning ② - 自然言語処理編(斎藤 康毅 著)
    • p.244

LSTM は、RNN における勾配消失問題を緩和するために、セル状態 $c_t$複数のゲート機構 を導入したモデルである。

1 ステップにおける LSTM の計算は、以下のように整理できる。

まず、入力 $x_t$ と直前の隠れ状態 $h_{t-1}$ に対して、4 種類のアフィン変換を行う。

  • 忘却ゲート

$$ f_t = \sigma(x_t W_x^{(f)} + h_{t-1} W_h^{(f)} + b^{(f)}) $$

  • 入力候補

$$ g_t = \tanh(x_t W_x^{(g)} + h_{t-1} W_h^{(g)} + b^{(g)}) $$

  • 入力ゲート

$$ i_t = \sigma(x_t W_x^{(i)} + h_{t-1} W_h^{(i)} + b^{(i)}) $$

  • 出力ゲート

$$ o_t = \sigma(x_t W_x^{(o)} + h_{t-1} W_h^{(o)} + b^{(o)}) $$

次に、これらのゲートを用いてセル状態を更新する。

  • セル状態の更新

$$ c_t = f_t \odot c_{t-1} + g_t \odot i_t $$

ここで、$\odot$ は要素ごとの積(アダマール積)を表す。

最後に、セル状態から隠れ状態を計算する。

  • 隠れ状態

$$ h_t = o_t \odot \tanh(c_t) $$

このように、LSTM では

  • 忘却ゲート $f_t$ により過去情報の保持量を制御し
  • 入力ゲート $i_t$ と入力候補 $g_t$ により新しい情報を書き込み
  • 出力ゲート $o_t$ により外部への出力を制御する

という役割分担が、計算式レベルで明確に分離されている。

この構造により、セル状態 $c_t$ は加算を中心とした更新となり、勾配が時間方向に減衰しにくい経路(CEC)として機能する。


3. GRU

GRU(Gated Recurrent Unit)は、LSTM を簡略化した再帰型モデルである。

GRU では、

  • セル状態と隠れ状態を統合
  • ゲート数を削減

することで、構造を単純化しつつ LSTM に近い性能を実現している。

計算量やパラメータ数を抑えたい場合に有効であり、タスクによっては LSTM と同等の性能を示す。

実装演習

なし。

確認テスト

LSTM と CEC が抱える課題について、それぞれ簡潔に述べよ

LSTM が抱える課題 は、構造が複雑でパラメータ数が多く、計算コストおよび学習コストが大きくなりやすい点である。

入力ゲート・忘却ゲート・出力ゲートに加えてセル状態を持つため、単純な RNN と比較して計算量が増加し、学習に時間がかかる。また、データ量が少ない場合には過学習を起こしやすいという課題もある。

CEC(Constant Error Carousel)が抱える課題 は、情報を長期間保持できる一方で、不要な情報まで保持し続けてしまう可能性がある点である。

CEC は誤差を減衰させずに伝播させる構造を持つが、そのままでは情報の取捨選択ができないため、重要でない情報がセル状態に残り続ける問題が生じる。

このため、LSTM では CEC 単体ではなく、忘却ゲートなどのゲート機構を組み合わせることで、必要な情報のみを保持し、不要な情報を適切に除去できるようにしている。

LSTM と GRU の違いを簡潔に述べよ

LSTM と GRU の違い は、主に構造の複雑さとゲート機構にある。

LSTM は、セル状態(CEC)を中心に、入力ゲート・忘却ゲート・出力ゲート の 3 つのゲートを用いて情報の書き込み・保持・出力を制御する。

この構造により長期依存関係を安定して学習できる一方、パラメータ数が多く、計算コストが高いという特徴を持つ。

一方、GRU は 更新ゲートとリセットゲート の 2 つのゲートを用い、セル状態と隠れ状態を統合した簡略化構造を持つ。

LSTM と比べてパラメータ数が少なく、学習や計算が高速である点が特徴である。

このように、表現力と制御の細かさを重視する場合は LSTM、計算効率や実装の簡潔さを重視する場合は GRU が適している。

参考図書 / 関連記事

  • ゼロから作る Deep Learning ② - 自然言語処理編(斎藤 康毅 著)
    • p.394

GRU(Gated Recurrent Unit)は、LSTM を簡略化した再帰型ニューラルネットワークであり、セル状態を持たず、隠れ状態のみで情報を管理する 構造を持つ。

LSTM が「隠れ状態」と「記憶セル」の 2 系統を扱うのに対し、GRU は隠れ状態 $h_t$ のみを用いる点が特徴である。

GRU における 1 ステップの計算は、以下のように整理できる。

まず、更新ゲート $z_t$ とリセットゲート $r_t$ を計算する。

  • 更新ゲート

$$ z_t = \sigma(x_t W_x^{(z)} + h_{t-1} W_h^{(z)} + b^{(z)}) $$

  • リセットゲート

$$ r_t = \sigma(x_t W_x^{(r)} + h_{t-1} W_h^{(r)} + b^{(r)}) $$

次に、リセットゲートを用いて中間状態 $\tilde{h}_t$ を計算する。

  • 中間状態

$$ \tilde{h}_t = \tanh(x_t W_x + (r_t \odot h_{t-1}) W_h + b) $$

ここで、$\odot$ は要素ごとの積(アダマール積)を表す。

最後に、更新ゲートを用いて隠れ状態を更新する。

  • 隠れ状態の更新

$$ h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t $$

この式から分かるように、更新ゲート $z_t$ は

  • 過去の隠れ状態 $h_{t-1}$ をどの程度保持するか
  • 新しく計算した中間状態 $\tilde{h}_t$ をどの程度採用するか

を制御する役割を持つ。

GRU は、LSTM と比較してゲート数が少なく、計算式も単純であるため、計算コストが低く、学習が高速になりやすい という利点を持つ。一方で、セル状態を明示的に分離していないため、タスクによっては LSTM の方が安定した性能を示す場合もある。


4. 双方向 RNN

双方向 RNN は、過去から未来への情報未来から過去への情報 を同時に扱うモデルである。

通常の RNN では、過去の情報しか利用できないが、双方向 RNN では系列全体を文脈として扱うことができる。

特に自然言語処理において、文全体の意味を考慮する必要がある場合に有効である。

実装演習

なし。

確認テスト

なし。

参考図書 / 関連記事

  • ゼロから作る Deep Learning ② - 自然言語処理編(斎藤 康毅 著)
    • p.360 - 362

双方向 RNN(Bidirectional RNN)は、系列データを 順方向と逆方向の両方から同時に処理 することで、各時刻における表現に前後の文脈情報を反映させる手法である。

特に自然言語処理の分野では、単語の意味が前後関係に強く依存するため、双方向処理が有効であることが知られている。

通常の RNN や LSTM では、系列を左から右へ一方向に処理するため、ある単語に対応する隠れ状態には、その単語より前の情報しか含まれない。

これに対し、双方向 RNN では、

  • 左から右に処理する RNN
  • 右から左に処理する RNN

の 2 つを並列に用い、それぞれの隠れ状態を統合することで、各時刻の表現に両方向の文脈情報を含めることができる。

実装上は、同一構造の RNN(または LSTM)を 2 つ用意し、一方には元の系列を、もう一方には逆順にした系列を入力する。その後、各時刻における 2 つの隠れ状態を 連結(concatenate) することで、最終的な隠れ状態ベクトルを得る方法が一般的である。

この点から、双方向 RNN は新しい計算構造を導入するというよりも、既存の RNN を組み合わせた拡張手法 として理解できる。

なお、双方向 RNN は系列全体を事前に観測できることを前提とするため、リアルタイム処理やオンライン推論には不向きである。一方で、機械翻訳や文章分類、系列全体を一括で処理できるタスクにおいては、高い表現力を発揮する。

このように、双方向 RNN は

  • 系列全体の文脈を考慮した表現が必要な場合に有効であり
  • Attention 機構と組み合わせることで、より精度の高い系列変換を実現できる

という点で、seq2seq や Attention モデルの Encoder 側を強化する重要な技術として位置づけられる。


5. seq2seq

seq2seq(Sequence to Sequence)モデルは、系列から系列への変換を目的としたモデルである。

Encoder と Decoder という二つの RNN から構成される。

5.1. Encoder RNN

Encoder RNN は、入力系列を処理し、その情報を内部状態(文脈ベクトル)に圧縮する。

5.2. Decoder RNN

Decoder RNN は、Encoder が生成した文脈ベクトルをもとに、出力系列を逐次生成する。

5.3. HRED

HRED(Hierarchical Recurrent Encoder-Decoder)は、文や発話を階層的に扱う seq2seq モデルである。

5.4. VHRED

VHRED は、HRED に確率的潜在変数を導入したモデルであり、出力の多様性を表現可能にしている。

5.5. VAE

5.5.1. オートエンコーダー

オートエンコーダーは、入力を一度低次元空間に写像し、再構成するモデルである。

5.5.2. VAE

VAE(Variational Autoencoder)は、潜在変数を確率分布として扱う確率的オートエンコーダーである。

実装演習

なし。

確認テスト

下記の選択肢から、seq2seq について説明しているものを選べ

正解は (2) である。

seq2seq は、RNN を用いた Encoder–Decoder モデル の一種であり、入力系列を Encoder で内部表現に変換し、その表現を用いて Decoder が出力系列を生成する構造を持つ。

このため、機械翻訳や文章生成など、系列から系列への変換を行うタスクに利用される。

なお、

  • (1)は双方向 RNN の説明
  • (3)は構文木を用いる再帰型ニューラルネットワーク(Recursive Neural Network)の説明
  • (4)は LSTM の説明

である。

seq2seq と HRED、HRED と VHRED の違いを簡潔に述べよ

seq2seq と HRED の違い は、文脈の扱い方にある。

seq2seq は単一の入力系列を Encoder で固定長ベクトルに変換し、それを用いて Decoder が出力系列を生成するモデルであり、各入力文を独立に扱う構造である。

一方、HRED(Hierarchical Recurrent Encoder-Decoder) は、文単位の Encoder に加えて、複数文の流れを扱う **上位の RNN(文脈 RNN)**を持つ階層構造を採用している。

これにより、対話などにおいて過去の発話履歴を文脈として保持しながら応答を生成できる。

HRED と VHRED の違い は、確率的表現の有無にある。

VHRED(Variational HRED)は、HRED に 潜在変数 を導入し、文脈表現を確率分布として扱うモデルである。

この構造により、同一の文脈に対しても多様な応答を生成でき、出力の単調化を防ぐことが可能となる。

まとめると、

  • seq2seq は単文処理向けの基本モデル
  • HRED は文脈を考慮した階層型モデル
  • VHRED は文脈に確率的多様性を導入した拡張モデル

である。

VAE に関する下記の説明文中の空欄に当てはまる言葉を答えよ

空欄に当てはまる語は 確率分布 である。

VAE は、自己符号化器(オートエンコーダ)の潜在変数に 確率分布 を導入したモデルであり、潜在空間を確率的に扱うことで、多様なデータ生成を可能にしている。

参考図書 / 関連記事

  • ゼロから作る Deep Learning ② - 自然言語処理編(斎藤 康毅 著)
    • p.287 - 291

seq2seq における Encoder–Decoder 構造の整理

seq2seq は、Encoder と Decoder の 2 つの RNN 系モデル を組み合わせることで、ある時系列データを別の時系列データへ変換するモデルである。機械翻訳を代表例とし、入力系列と出力系列の長さが異なる場合にも対応できる点が特徴である。

Encoder は、入力された文章を単語単位で処理し、時系列全体を 固定長のベクトル表現 $h$ に変換する役割を担う。具体的には、RNN(一般には LSTM や GRU)を用いて入力系列を順に処理し、最終時刻の隠れ状態を文章全体の要約表現として出力する。この処理は、可変長の文章を固定次元のベクトルへ圧縮する操作と捉えることができる。

Decoder は、Encoder が出力したベクトル $h$ を初期状態として受け取り、目的とする文章を 1 単語ずつ逐次的に生成 する。各時刻において、Decoder は直前に生成した単語と内部状態をもとに次の単語の確率分布を計算し、Softmax を通して出力単語を決定する。このとき、文の開始と終了を制御するために、特殊記号(例:<eos>)が用いられる。

seq2seq の特徴的な点は、Encoder と Decoder が 隠れ状態ベクトル $h$ を介して接続されている ことである。順伝播では、Encoder で圧縮された情報が Decoder に渡され、逆伝播では、その経路を通じて誤差が Encoder 側へと伝播する。このため、Encoder と Decoder は独立したモデルではなく、一体のネットワークとして同時に学習される。

一方で、この構造では、入力系列のすべての情報を固定長ベクトル $h$ に集約する必要があるため、文章が長くなると情報の欠落が生じやすい。この問題を緩和するために、後に Attention 機構が導入され、Encoder の各時刻の隠れ状態を Decoder が直接参照できるよう拡張されている。


6. word2vec

word2vec は、単語を高次元ベクトルとして表現する手法である。

単語間の意味的類似性をベクトル空間上の距離として表現できる点が特徴であり、RNN や seq2seq における入力表現として広く用いられる。

実装演習

なし。

確認テスト

なし。

参考図書 / 関連記事

  • ゼロから作る Deep Learning ② - 自然言語処理編(斎藤 康毅 著)
    • p.101 - 106

word2vec は、単語を固定長の実数ベクトルとして表現する 分散表現(word embedding) を学習する手法である。本来はツール名であるが、一般にはその内部で用いられる CBOW モデル および skip-gram モデル を含めて指すことが多い。

CBOW(Continuous Bag-of-Words)モデルは、周囲の単語(コンテキスト)から中央の単語を予測することを目的としたニューラルネットワークである。入力は one-hot 表現された複数のコンテキスト単語であり、それぞれが同一の重み行列 $W_{\text{in}}$ によって変換される。

入力層から中間層への変換は、全結合層ではなく 行列積(MatMul) として実装される。このとき、one-hot ベクトルと $W_{\text{in}}$ の積は、対応する単語の行ベクトルを取り出す操作に等しい。すなわち、$W_{\text{in}}$ の各行が、各単語の分散表現そのものに対応している。

複数のコンテキスト単語が与えられる場合、それぞれの単語ベクトルは平均され、中間層の表現となる。この平均操作により、CBOW モデルは 単語順序を考慮せず、周辺語の集合(bag-of-words)として文脈を扱う構造を持つ。

中間層から出力層への変換では、別の重み行列 $W_{\text{out}}$ が用いられ、各単語に対応するスコアが計算される。これらのスコアに Softmax 関数を適用することで、中央単語が各語である確率分布が得られる。学習では、正解単語の確率が高くなるように重みが更新される。

この学習過程を通じて、意味的に似た文脈で出現する単語同士は、似たベクトル表現を持つように $W_{\text{in}}$ が更新される。最終的に得られる単語ベクトルは、人間が直接解釈できるものではないが、意味情報を圧縮して保持する表現となる。この点で、CBOW モデルは「エンコード」を通じて意味を数値化し、「デコード」を通じて予測を行う構造を持つと捉えることができる。


7. Attention Mechanism

Attention Mechanism は、seq2seq における 固定長ベクトルの限界 を解決するために提案された。

出力生成時に、入力系列のどの部分に注目すべきかを重みとして計算することで、長い系列に対しても有効な情報参照が可能となる。

実装演習

なし。

確認テスト

RNN と word2vec、seq2seq と Attention の違いを簡潔に述べよ

RNN は、時系列データを扱うためのニューラルネットワークであり、過去の入力を隠れ状態として保持しながら系列全体を処理するモデルである。主に文脈情報を考慮した系列処理を目的とする。

word2vec は、単語を意味的な特徴を持つ固定長のベクトルに変換する手法であり、文脈を直接処理するモデルではない。単語間の意味的類似性をベクトル空間上で表現することを目的とし、RNN などのモデルへの入力表現として利用される。

seq2seq+Attention は、Encoder–Decoder 構造を持つ seq2seq モデルに Attention 機構を導入したものである。出力生成時に入力系列の各要素へ重み付けを行い、重要な情報を動的に参照できるため、固定長ベクトルに依存する RNN や通常の seq2seq よりも長い系列に対して高い性能を示す。

まとめると、

  • RNN は系列処理の基盤モデル
  • word2vec は単語の分散表現手法
  • seq2seq+Attention は系列変換を高精度に行う応用モデル

である。

参考図書 / 関連記事

  • ゼロから作る Deep Learning ② - 自然言語処理編(斎藤 康毅 著)
    • p.325 - 348

Attention 導入の動機

従来の seq2seq モデルでは、Encoder が入力文全体を 固定長のベクトル に圧縮し、その情報を Decoder に渡すことで文章生成や翻訳を行っていた。

しかしこの構造には、入力文が長くなるほど重要な情報が失われやすいという問題がある。特に自然言語処理では、文の後半に現れる単語や、文脈上重要な語が、固定長ベクトルへの圧縮過程で十分に反映されない場合がある。

この問題は「長文になるほど性能が劣化する」「入力文のどの部分を参照して出力しているのかが不明瞭である」といった形で顕在化する。

人間が翻訳や文章理解を行う際には、生成中の単語に応じて原文中の異なる箇所に注目しているが、従来の seq2seq モデルではこのような柔軟な参照ができなかった。

この固定長表現の制約を解消するために導入されたのが Attention 機構 である。

Attention の基本的な考え方

Attention 機構の本質は、「Decoder が出力を生成する各時刻において、Encoder のどの時刻の情報をどの程度参照するかを動的に決定する」点にある。

Encoder は入力系列の各時刻ごとに隠れ状態ベクトルを出力する。Attention を用いない場合、これらの情報は最終時刻の隠れ状態に集約されるが、Attention 機構では 全時刻の隠れ状態を保持したまま Decoder に渡す。

Decoder は、現在の内部状態と Encoder の各隠れ状態との「関連度」を計算し、それらを重みとして正規化する。この重みは Attention 重み と呼ばれ、どの入力単語にどれだけ注目しているかを数値として表す。

得られた重みを用いて Encoder の隠れ状態を加重平均したベクトルを コンテキストベクトル とし、これを Decoder の次の出力計算に利用する。これにより、Decoder は出力単語ごとに異なる入力位置の情報を重点的に参照できるようになる。

Attention 機構の仕組み(概念的説明)

Attention の計算は、大きく以下の流れで行われる。

  1. スコア計算

    Decoder の現在の状態と、Encoder の各時刻の隠れ状態との類似度を計算する。

  2. 正規化

    得られたスコアに Softmax 関数を適用し、重みの総和が 1 となるように正規化する。

  3. 重み付き和の計算

    正規化された重みを用いて Encoder の隠れ状態を加重平均し、コンテキストベクトルを生成する。

  4. 出力生成への利用

    このコンテキストベクトルを Decoder の入力や出力計算に組み込み、次の単語を生成する。

この一連の処理を Decoder の各時刻で繰り返すことで、出力単語ごとに参照する入力情報を柔軟に切り替えることが可能になる。

Attention 導入による効果

Attention 機構を導入することで、以下のような利点が得られる。

  • 入力文が長くなっても情報を保持しやすくなる
  • 出力単語と入力単語の対応関係を明示的に扱える
  • 翻訳や要約などで性能が大きく向上する
  • 「どの単語に注目しているか」を可視化でき、モデルの解釈性が向上する

このように、Attention は単なる性能向上のための工夫ではなく、系列変換モデルの表現能力そのものを拡張する重要な機構である。

まとめ

Attention 機構は、従来の seq2seq モデルが抱えていた「固定長ベクトルによる情報圧縮」という制約を克服するために導入された。

Encoder が出力する各時刻の情報を保持し、Decoder が必要に応じて参照先を選択することで、より柔軟で高精度な系列変換を実現している。

この考え方は後に Transformer へと発展し、現在の自然言語処理モデルの基盤となっている。

8. VQ-VAE

8.1. VQ-VAE 導入の背景と動機

変分オートエンコーダ(VAE)は、入力データを潜在変数へ写像し、その潜在空間からデータを生成する確率的生成モデルである。

しかし、通常の VAE では潜在変数が連続値で表現されるため、画像や音声に含まれる離散的・カテゴリ的な構造を十分に捉えにくいという課題がある。

また、Encoder が出力する潜在変数が Decoder に十分利用されなくなる posterior collapse と呼ばれる問題も知られている。

これらの問題に対し、潜在表現を離散化することで表現力を高めることを目的として提案されたのが VQ-VAE(Vector Quantized Variational AutoEncoder) である。

VQ-VAE は、潜在変数を連続空間ではなく、有限個の埋め込みベクトルから選択される離散表現として扱う点に特徴がある。

8.2. VQ-VAE のモデル構成

VQ-VAE は、Encoder、Vector Quantization(コードブック)、Decoder の三つの要素から構成される。

まず Encoder は、入力データ $x$ を連続値の潜在表現 $z_e(x)$ に変換する。次に、この $z_e(x)$ はコードブックと呼ばれる有限個の埋め込みベクトル集合と比較され、最も距離が近いベクトルが選択される。この処理により、潜在表現は

$$ z_q(x) = e_k $$

という離散的な表現へと量子化される。最後に Decoder は、この量子化された潜在表現 $z_q(x)$ を用いて元の入力データの再構成を行う。

このように VQ-VAE では、潜在空間が明示的に離散化されており、入力データが「コード列」として表現される点が従来の VAE と大きく異なる。

8.3. 損失関数の構造

VQ-VAE の学習は、以下の三つの損失項から構成される。

$$ \mathcal{L} = \log p(x \mid z_q(x)) + \| \mathrm{sg}[z_e(x)] - e \|^2 + \beta \| z_e(x) - \mathrm{sg}[e] \|^2 $$

第一項は 再構成誤差 であり、Decoder が入力データをどれだけ正確に再現できているかを評価する。

第二項は コードブック損失 であり、選択された埋め込みベクトルが Encoder の出力に近づくよう更新される。この項では Encoder 側への勾配伝播は遮断される。

第三項は コミットメント損失 であり、Encoder の出力が特定の埋め込みベクトルに過度に離れないよう制約を与える役割を持つ。係数 $\beta$ はこの制約の強さを調整するハイパーパラメータである。

ここで用いられる $\mathrm{sg}[\cdot]$ は stop gradient を意味し、特定の経路での勾配伝播を遮断するために用いられる。

8.4. 勾配伝播と Straight-Through Estimator

ベクトル量子化処理は最近傍探索を含むため、数学的に微分不可能である。この問題に対処するため、VQ-VAE では straight-through estimator が用いられる。

具体的には、順伝播では量子化された $z_q(x)$ を Decoder に入力するが、逆伝播ではその勾配をそのまま Encoder の出力 $z_e(x)$ に流す。この近似により、量子化を含むモデルであっても Encoder と Decoder を同時に学習することが可能となっている。

8.5. VQ-VAE の意義と位置づけ

VQ-VAE は、VAE の枠組みを保ちつつ、潜在表現を離散化することで表現力を向上させたモデルである。KL ダイバージェンスを用いずに学習できる点や、潜在表現が明示的なコードとして扱える点は、画像生成や音声生成において大きな利点となる。

この考え方は、後続の VQ-VAE-2 や音声生成モデルなどへと発展しており、離散潜在表現に基づく生成モデルの基盤技術として重要な位置を占めている。

実装演習

なし。

確認テスト

なし。

参考図書 / 関連記事

VQ-VAE の特徴の一つとして、生成過程を二段階に分離している点が挙げられる。論文では

Whilst training the VQ-VAE, the prior is kept constant and uniform. After training, we fit an autoregressive distribution over z, p(z), so that we can generate x via ancestral sampling.

と述べられており、VQ-VAE 本体の学習中は prior を一様分布に固定し、潜在表現の獲得に専念する一方で、その後に潜在変数 z 上で自己回帰モデルによる prior を別途学習する、という構成が明確に示されている。

この構成により、生成時にはまず離散潜在空間上で自己回帰モデルによって潜在変数 z を生成し、その後 Decoder によってデータ空間へ写像するという二段階の生成過程が実現される。実際に論文中では、

Samples drawn from the PixelCNN were mapped to pixel-space with the decoder of the VQ-VAE.

と説明されており、生成処理が潜在空間とデータ空間の二段階で実行されていることが明示されている。

ここで重要なのは、VQ-VAE 本体は意味的に有用な離散潜在表現を学習する役割を担い、その潜在表現上に自己回帰的な prior を学習することで、はじめて意味的に一貫した生成が可能になる、という点である。

すなわち、VQ-VAE は高次元なデータ空間で直接生成を行うのではなく、一度学習された意味的に圧縮された潜在表現の空間上で生成を行い、その結果を Decoder によってデータ空間へ展開する、という設計思想を採用している。

このような prior と Decoder の役割分離は、強力な自己回帰モデルを Decoder 側に置くことで生じやすい posterior collapse を回避しつつ、高次元データの長距離構造を効率的にモデル化するための、VQ-VAE を特徴づける重要な設計思想である。


9. [フレームワーク演習] 双方向 RNN / 勾配のクリッピング

双方向 RNN は、時系列データを 過去から未来方向未来から過去方向 の両方向から同時に処理する構造を持つ。

通常の RNN では、各時刻の出力は過去の情報のみに基づいて計算されるが、双方向RNNでは系列全体の文脈を考慮した特徴表現を得ることが可能となる。音声認識のように、ある時刻の情報が前後の音素に強く依存するタスクにおいては、この双方向性が有効に働く。

一方、RNN 系モデルでは、時系列が長くなるにつれて 勾配爆発勾配消失 が発生しやすいという問題がある。この演習では、この問題に対する代表的な対策として 勾配のクリッピング を導入している。

勾配クリッピングとは、逆伝播時に計算される勾配の大きさがあらかじめ定めた閾値を超えた場合に、その値を制限する手法である。

実装では、最適化手法として Adam を用い、その引数として clipvalue を指定することで勾配クリッピングを適用している。これにより、学習初期や不安定な状況においてもパラメータ更新が過度に大きくなることを防ぎ、学習の発散を抑制する効果が期待できる。

このように、双方向RNNによる 文脈情報の拡張 と、勾配クリッピングによる 学習の安定化 は、いずれも実用的な時系列モデルを構築する上で重要な技術であり、本演習はそれらをフレームワークレベルで確認する内容となっている。

実装演習

3_4_spoken_digit.ipynb を実行した。

  • 双方向 RNN (LSTM) を使用したモデル

stage_3_3_05.png

model.summary() から、双方向 RNN では順方向と逆方向の RNN の出力が結合されるため、通常の RNN や LSTM と比較して出力次元およびパラメータ数が増加していることが確認できる。

双方向 RNN は、系列データを過去から未来方向だけでなく、未来から過去方向にも同時に処理することができるため、入力系列全体の文脈情報をより豊かに利用できるという利点を持つ。特に音声認識のように、前後の時間情報が重要となるタスクにおいて有効である。

一方で、パラメータ数の増加に伴い計算量が増大するため、学習コストが高くなる点には注意が必要である。本結果から、双方向 RNN は表現力を高める一方で、モデルの複雑化を伴う手法であることが確認できた。

  • 勾配のクリッピング

stage_3_3_06.png

勾配クリッピングは、逆伝播時に勾配の大きさを一定範囲に制限することで、勾配爆発を防止する手法である。

実行結果から、勾配クリッピングを設定したモデルでは学習が途中で発散することなく、安定して最後まで進行していることが確認できた。これは、RNN 系モデルで問題となりやすい勾配爆発が抑制され、パラメータ更新が過度にならなかったためであると考えられる。

このことから、勾配クリッピングは RNN や LSTM などの系列モデルにおいて、学習を安定化させるための有効な手法であることが分かる。

確認テスト

なし。

参考図書 / 関連記事

  • ゼロから作る Deep Learning ② - 自然言語処理編(斎藤 康毅 著)
    • p.232 - 233

勾配クリッピングは、RNN などの系列モデルにおいて問題となる 勾配爆発 への代表的な対策である。

逆伝播によって得られた勾配ベクトル全体のノルムが、あらかじめ定めたしきい値を超えた場合に、勾配の大きさを一定範囲内に収めるよう正規化を行う。

具体的には、全パラメータに対応する勾配を一つのベクトル $\boldsymbol{g}$ とみなし、その L2 ノルム $\| \boldsymbol{g} \|$ が $\text{threshold}$ を超えた場合、

$$ \boldsymbol{g} \leftarrow \frac{\text{threshold}}{\| \boldsymbol{g} \|}\boldsymbol{g} $$

のようにスケーリングを行う。この処理により、勾配の方向を保ったまま大きさのみを制限できるため、学習の発散を防ぎつつ安定した更新が可能となる。

勾配クリッピングはアルゴリズムとして単純でありながら、多くの RNN・LSTM 系モデルで有効に機能する手法である。特に系列長が長くなる場合や、学習初期に勾配が不安定になりやすい状況において、学習を継続可能な範囲に保つための基本的な対策として位置づけられる。


10. [フレームワーク演習] seq2seq

seq2seq(Sequence to Sequence)モデルは、入力系列を別の系列へ変換するためのニューラルネットワーク構造であり、主に Encoder–Decoder 構成 を採用する点に特徴がある。代表的な応用例として、機械翻訳、時系列予測、音声認識などが挙げられる。

Encoder は入力系列を時系列順に処理し、その情報を内部状態(隠れ状態)として要約する役割を担う。一方、Decoder は Encoder が出力した最終的な隠れ状態を初期状態として受け取り、目的とする出力系列を逐次生成する。この構造により、入力系列と出力系列の長さが異なる問題にも対応可能となる。

この演習では、sin 波を入力として cos 波を出力するタスクを通じて、seq2seq モデルの基本的な挙動を確認している。このような連続値の系列変換においても、Encoder が入力系列の時系列的特徴を内部表現に圧縮し、Decoder がその表現をもとに出力系列を再構成できることが示されている。

ただし、標準的な seq2seq モデルでは、入力系列全体の情報を固定長のベクトルに圧縮する必要があるため、系列が長くなるにつれて情報損失が生じやすいという課題がある。この問題は、後に導入される Attention 機構によって改善されることが知られている。

以上より、seq2seq は系列変換問題の基本構造を理解するうえで重要なモデルであり、Attention や Transformer といった発展的手法の基礎となる位置付けにある。

実装演習

3_5_Seq2Seq(Encoder-Decoder)_sin-cos.ipynb を実行した。

ここだけ、コードを修正する。

pred_d_state_in = tf.keras.layers.Input(shape=(NUM_HIDDEN_PARAMS,))
  • 学習の進行状況

stage_3_3_07.png

学習損失(loss)と検証損失(val_loss)は、ともに初期エポックで急激に減少し、その後は緩やかに低下しながら安定して収束している。この挙動から、Encoder–Decoder 型の seq2seq モデルが sin–cos の時系列変換タスクを適切に学習できていることが確認できる。

また、学習損失と検証損失の間に大きな乖離は見られず、過学習の兆候はほとんど確認されない。これは、タスクが比較的単純な連続値回帰問題であり、モデル容量とデータ量のバランスが適切であったためと考えられる。

  • 推論

stage_3_3_08.png

出力された波形(output)は、正解波形(correct)である cos 波と全体的に良く一致しており、振幅や周期といった特徴を適切に再現できていることが分かる。特に位相のずれが小さく、Encoder が入力系列(sin)から必要な時系列情報を潜在状態に圧縮し、Decoder がそれを用いて連続値系列を生成できている点は、seq2seq モデルの有効性を示している。

一方で、局所的にはピーク付近や谷付近でわずかな誤差が見られる。これは、固定長の潜在ベクトルに時系列全体の情報を圧縮しているため、細かな時間依存構造の再現に限界があることが原因と考えられる。この点は、Attention 機構を導入することで改善される典型的な課題である。

確認テスト

なし。

参考図書 / 関連記事

  • ゼロから作る Deep Learning ② - 自然言語処理編(斎藤 康毅 著)
    • p.310 - 313

seq2seq モデルでは、入力系列を固定長の内部表現に圧縮する構造上、系列の先頭に位置する情報ほど出力との距離が長くなり、勾配が伝わりにくくなるという問題がある。これにより、学習の進行が遅くなり、精度が伸びにくくなる場合がある。

この問題に対する簡易的な改良手段の一つが、入力系列を時間方向に反転させる 方法である。入力を反転させることで、出力に強く関係する入力要素が Decoder に近い位置に配置され、逆伝播時の勾配がより直接的に伝わるようになる。

テキストに掲載されている実験結果からも、入力系列を反転させただけで学習の進みが明確に改善し、正解率が大きく向上することが確認できる。これはモデル構造を変更せず、前処理のみで効果が得られる点で実用的である。

以上より、入力系列の反転は seq2seq モデルにおける 勾配伝播を改善するための有効な工夫の一つ であり、Attention 機構のような構造的改良が導入される以前に用いられてきた、基本的かつ重要な手法である。


11. [フレームワーク演習] data-augmentation

11.1. Data Augmentation の目的

Data Augmentation(データ拡張)とは、既存の学習データに変換を加えて擬似的にデータ数を増やす手法である。

主な目的は以下のとおりである。

  • 学習データ不足の緩和
  • 過学習(訓練データへの過度な適合)の抑制
  • 入力のわずかな変動に対する 頑健性(ロバスト性) の向上

特に画像認識や音声認識などでは、実データ収集が困難なため重要な手法となる。

11.2. Data Augmentation の基本的な考え方

データ拡張では、ラベルの意味を保ったまま入力のみを変形する。

例(画像)

  • 回転
  • 平行移動
  • 拡大・縮小
  • 左右反転
  • 明るさ・コントラスト変更

これにより、モデルは「特定の見た目」ではなく「本質的な特徴」を学習しやすくなる。

11.3. フレームワーク(TensorFlow)での実装位置

TensorFlow / Keras では、Data Augmentation は以下の形で実装されることが多い。

  • tf.keras.layers による、前処理レイヤとしての組み込み
  • 学習時のみ有効(推論時は適用されない)

これにより、

  • 学習データを事前に保存し直す必要がない
  • GPU 上で効率よく処理できる

という利点がある。

11.4. Data Augmentation の効果と注意点

効果

  • validation loss の悪化を抑制しやすい
  • 汎化性能が向上しやすい

注意点

  • 変換が強すぎると、元のラベルと意味的に矛盾する場合がある
  • すべてのタスクに有効とは限らない(例:数値データ)

そのため、「どの変換がタスクに適切か」を意識して設計する必要がある。

実装演習

3_6_data_augmentation_with_tf_add2.ipynb を実行した。

今回のノートブックは、そのままだと動かなかったため、以下のような修正を加えた。

  • RandAugment で、imgaug を使用せず Keras 標準の Data Augmentation を使用する
  • EDA で、stop_words が Pandas で読み込めないため、strip() で処理する

RandAugment

stage_3_3_09.png

RandAugment は、回転・平行移動・色変換などの複数の変換をランダムに合成して適用するため、同一ラベルのまま見た目が大きく異なる画像を作れる。

これにより、モデルが「背景・撮影条件・色味」といった偶然の要素に過適合しにくくなり、汎化性能(未知データへの強さ)を上げる方向に働く。

一方で、変換が強すぎると本来の識別に必要な手掛かりまで壊れる(例:物体の形状が崩れて別物に見える)ため、強度(magnitude)や適用回数はデータセットに合わせて調整が必要となる。

Synonym Replacement

stage_3_3_10.png

スクショの例では「類似→似る」「する→遣る」のように同義語置換が起きていますが、日本語ではこの手法が 意味を壊しやすい のが注意点である。

  • 良い点:
    • 語彙の揺れ(言い換え)にモデルを慣らし、表現が少し違っても同じ意味として扱えるようにする効果が期待できる
  • 悪い点:
    • 日本語は 活用・品詞制約 が強く、単純な単語置換だと文法が崩れやすい
    • 同義語辞書の粒度次第で「する→造る」のような 文脈不一致 が起き、ラベルノイズになり得る

実務的には、置換対象を「名詞・形容詞中心にする」「助詞や汎用動詞(する等)は除外」「置換候補の品詞一致を条件にする」などの制約を入れると安定する。

Random Insertion

stage_3_3_11.png

例では「行う」「保存」などが挿入され、文が「類似行うするデータ保存を…」のように不自然になっている。

  • 良い点:文の一部に余計な語が入っても意味を保つような頑健性(ノイズ耐性)を与えられる
  • 悪い点:挿入位置がランダムだと、特に日本語では 語順・係り受け が崩れて意味が変わりやすい

そのため、挿入は「同義語を近傍に入れる」「句読点単位で入れる」など、挿入位置のルール がないとノイズが強すぎて学習を邪魔する可能性がある。

Random Swap

stage_3_3_12.png

スクショでは「を⇔類似」「する⇔ます」などが入れ替わり、文がほぼ崩壊している。

Swap は語順を崩すため、英語のように語順依存が比較的弱い言語でも効果は限定的なことがあるが、日本語では助詞が重要で、助詞や語尾を入れ替えると意味が壊れやすい。

したがって日本語で使うなら、

  • スワップ対象を「内容語(名詞など)だけ」に限定
  • 「助詞・助動詞・句読点は対象外」のような制限がほぼ必須。制限なしだとデータ品質が落ち、性能低下に繋がり得る

Random Deletion

stage_3_3_13.png

例では「ます」が削除されて「…書いて。」となっている。

  • 良い点:一部の語が欠落してもクラス判定できるようにする(入力の欠損への頑健性)
  • 悪い点:削除確率が高いと、情報量が足りずに ラベルを支えられない文 が増える。特に短文では致命的になりやすい

実用上は p をかなり小さく(短文ほど小さく)し、さらに「全部消える」ケースをフォールバックで救う(今回入れている処理)は妥当。

  • EDA

stage_3_3_14.png

EDA は SR/RI/RS/RD を混ぜて複数文を生成するため、単一手法より多様性 が出る。

  • 良い点:データが少ないときに、軽量にバリエーションを増やせる(特にベースラインの底上げに向く)
  • 注意点:日本語では上述の通り、制約なしだと文法崩壊が起きやすく、水増ししたつもりがノイズを増やす 危険がある

そのため、EDA を日本語で使う場合は、

  • 置換/挿入の対象を内容語に限定
  • 助詞や「する」のような汎用語を除外
  • 生成文を目視 or ルールでフィルタ(文字数下限、記号だらけ除外など)といった品質管理が重要、という結論になる

確認テスト

なし。

参考図書 / 関連記事

以下は、1. Introduction より引用。

We demonstrate that the optimal strength of a data augmentation depends on the model size and training set size. This observation indicates that a separate optimization of an augmentation policy on a smaller proxy task may be sub-optimal for learning and transferring augmentation policies.

RandAugment では、データ拡張の「強さ」は一律に決めるべきものではなく、モデルの規模やデータセットの大きさに依存する ことが指摘されている。実際、論文では小規模モデルと大規模モデルでは最適な拡張強度が異なることが実験的に示されている。

この結果は、従来の AutoAugment のように、小さな proxy task 上で探索した拡張方針をそのまま大規模タスクへ適用する方法が、必ずしも最適でないことを意味している。

そのため RandAugment では、探索フェーズ自体を廃し、拡張回数 $N$ と強度 $M$ という 解釈可能な少数のハイパーパラメータ を、対象タスクに直接適用する設計が採用されている。


12. [フレームワーク演習] activate_functions

活性化関数は、各層での線形結合に非線形性を与える役割を担っており、モデルの表現能力および学習の安定性に直接影響する重要な要素である。

Sigmoid 関数は出力を 0〜1 の範囲に収める性質を持つが、入力の絶対値が大きくなると勾配がほぼ 0 となるため、勾配消失が起きやすい。この性質により、深いネットワークでは学習が進みにくくなる問題が確認できる。

tanh 関数は出力が −1〜1 に分布し、平均が 0 に近づく点で Sigmoid より改善されているが、同様に飽和領域では勾配消失が発生する。

一方、ReLU 関数は正の領域で勾配が一定であり、勾配消失が起きにくいため、深層学習において広く用いられている。ただし、入力が負の領域に入ると出力が常に 0 となるため、ユニットが更新されなくなる「Dying ReLU」の問題が起こり得る点には注意が必要である。

これらの結果から、活性化関数は単に非線形性を導入するだけでなく、勾配の流れや学習効率を左右する要因であることが分かる。

実務的には、隠れ層には ReLU 系の関数を用い、出力層ではタスクに応じて Sigmoid や Softmax、恒等関数を使い分けることが一般的である。

Swish 活性化関数について

この演習で新たに扱った活性化関数として Swish がある。

Swish は Google によって提案された活性化関数であり、ReLU の単純さと Sigmoid 系の滑らかさを併せ持つ特徴を持つ。

Swish の定義式は次の通りである。

$$ \mathrm{Swish}(x) = x \cdot \sigma(x) $$

ただし、$\sigma(x)$ は Sigmoid 関数であり、

$$ \sigma(x) = \frac{1}{1 + e^{-x}} $$

で定義される。

この式から分かるように、Swish は入力 $x$ をそのまま通すのではなく、Sigmoid によって 入力に応じた重み付け を行った上で出力する関数である。

入力が大きく正の場合には $\sigma(x) \approx 1$ となり、出力はほぼ $x$ となる。一方、入力が負の場合でも $\sigma(x)$ は 0 にはならないため、ReLU のように完全に出力が遮断されることはない。

Swish の微分は次のように表される。

$$ \frac{d}{dx}\mathrm{Swish}(x) = \sigma(x) + x \cdot \sigma(x)\left(1 - \sigma(x)\right) $$

この式から、Swish は 全域で連続かつ滑らかな勾配 を持つことが分かる。 ReLU のような不連続点を持たず、負の領域でも勾配が完全に 0 にならないため、Dying ReLU の問題を緩和できるとされている。

また、Swish は単調増加関数ではなく、負の領域でわずかに出力が下がる非単調性を持つ。この性質により、ReLU よりも表現力が高くなる場合があり、深いネットワークにおいて性能向上が報告されている。

以上より、Swish は

  • ReLU の学習安定性
  • Sigmoid 系の滑らかさ

を併せ持つ活性化関数であり、特に深層モデルにおいて有効な選択肢となり得る。

実装演習

3_7_activation_functions.ipynb を実行した。

新たに出てきた Swish のみ、結果を確認する。

stage_3_3_15.png

図より、Swish は負の入力領域においても出力および勾配が連続的に変化しており、勾配が 0 に張り付かないことが確認できる。一方、正の領域では出力がほぼ線形に増加し、勾配も 1 付近に保たれている。

この挙動から、Swish は ReLU と同様の勾配伝播のしやすさを持ちつつ、負の領域での情報消失を抑える活性化関数であることが分かる。

確認テスト

なし。

参考図書 / 関連記事

活性化関数探索という視点

近年提案されている Swish 活性化関数は、従来のように人手で設計された関数ではなく、自動探索によって発見された活性化関数 である点に特徴がある。

Ramachandran らは、活性化関数の設計自体を探索問題として定式化し、強化学習および全探索を用いて有効な活性化関数を体系的に探索した。

これまでにも ReLU を置き換える活性化関数は多数提案されてきたが、多くの場合、特定のモデルやデータセットにおいてのみ有効であり、一貫した性能向上が得られない という問題があった。論文中でも、

none have managed to replace it due to inconsistent gains

と述べられており、ReLU が長年にわたって標準的な選択肢であり続けた理由が整理されている。

この背景を踏まえ、本研究では「どのような活性化関数が良いか」という人間の仮説に依存せず、関数の構造そのものを探索空間として定義 し、その中から性能の高いものを自動的に選択するという方針が採られている。

探索結果から得られた知見

論文では、探索によって得られた複数の活性化関数を比較し、性能の高い関数群に共通する性質が分析されている。その中で特に重要なのは、複雑な活性化関数ほど性能が高いわけではない という点である。

実際、探索結果からは、構造が過度に複雑な関数は最適化が困難となり、学習性能が低下しやすいことが報告されている。一方で、性能の高い活性化関数の多くは、1〜2 個の単純な演算から構成されており、Swish もその一例である。

Complicated activation functions consistently underperform simpler activation functions

という記述は、活性化関数設計において「表現力の高さ」よりも「最適化のしやすさ」が重要であることを示唆している。

ReLU の前提を再考する視点

さらに本論文では、Swish の成功が ReLU の持つ「勾配が 1 で流れる性質」が必須ではない可能性 を示している点も重要である。

残差接続や Attention 機構の導入によって、深層モデルにおける勾配伝播は構造的に改善されており、個々の活性化関数が厳密に勾配を保存する必要性は相対的に低下していると議論されている。

この点について論文では、

architectural improvements lessen the need for individual components to preserve gradients

と述べられており、活性化関数の役割が、従来の「勾配消失を防ぐための装置」から、「モデル全体の表現力を調整する構成要素」へと変化していることが示唆されている。

この演習との位置づけ

本演習で扱った Swish は、単に ReLU の代替として性能が良い活性化関数であるだけでなく、活性化関数設計そのものが自動化の対象となり得る ことを示した具体例である。

この点は、データ拡張やアーキテクチャ探索と同様に、深層学習における設計判断が経験則から探索・最適化へと移行しつつある流れを反映している。

以上より、Swish は活性化関数の性能向上という観点だけでなく、「人手による設計の限界」と「自動探索の有効性」 を示す事例としても重要であり、今後の深層学習モデル設計の方向性を考える上で示唆に富む手法である。

2025-12-19

ラビット・チャレンジ - Stage 3. 深層学習 前編 (Day 2)

提出したレポートです。

絶対書きすぎですが、行間を埋めたくなるので仕方ない。


Rabbit Challenge - Stage 3. 深層学習 前編 (Day 2)

1. 勾配消失問題

深層ニューラルネットワークでは、層を深くすることで表現力は向上する一方、学習がうまく進まなくなる問題が生じることがある。

その代表的な例が 勾配消失問題 である。

勾配消失問題とは、誤差逆伝播法によって勾配を計算する際、層を遡るにつれて勾配が小さくなり、入力層付近の重みがほとんど更新されなくなる現象を指す。

この問題が発生すると、理論的には表現力の高い深層モデルであっても、実際には十分な学習が行われない。

本講義では、勾配消失問題を 深層モデルの学習を妨げる主要な要因の一つ として位置づけ、その対策として、

  • 活性化関数の選択
  • 重みの初期値設定
  • 正規化手法の導入

がどのような役割を果たすかを順に確認する。

以下では、それぞれの手法について基本的な定義と数式を整理し、後続の実装演習への準備とする。

1.1 活性化関数:ReLU 関数

ReLU(Rectified Linear Unit)関数は、以下のように定義される活性化関数である。

$$ f(x) = \max(0, x) $$

この関数の微分は次のようになる。

$$ f'(x) = \begin{cases} 1 & (x > 0) \\ 0 & (x \le 0) \end{cases} $$

ReLU 関数は、正の入力に対して微分値が一定であるため、シグモイド関数や tanh 関数と比べて、逆伝播の過程で勾配が小さくなりにくい。

この性質により、活性化関数に起因する勾配消失を緩和し、深い層まで勾配を伝えやすくする点に意義がある。

1.2 重みの初期値設定:Xavier 初期化

重みの初期値が不適切な場合、順伝播や逆伝播の過程で信号や勾配の分散が層を重ねるごとに変化し、学習が不安定になる。

この問題を緩和するために用いられる手法が Xavier 初期化 である。

Xavier 初期化では、重み $W$ を次の分布から初期化する。

$$ W \sim \mathcal{N}\left(0,\ \frac{1}{n_{\text{in}}}\right) $$

または

$$ W \sim \mathcal{U}\left(-\sqrt{\frac{6}{n_{\text{in}}+n_{\text{out}}}},\ \sqrt{\frac{6}{n_{\text{in}}+n_{\text{out}}}}\right) $$

ここで、$n_{\text{in}}$ は入力ユニット数、$n_{\text{out}}$ は出力ユニット数である。

この初期化により、層をまたいでも出力や勾配の分散が大きく変化しないように調整される。

1.3 重みの初期値設定:He 初期化

He 初期化は、ReLU 関数を用いる場合に適した重み初期化手法である。

ReLU 関数では負の入力が $0$ になるため、有効なユニット数が減少する点を考慮している。

He 初期化では、重み $W$ を次の分布から初期化する。

$$ W \sim \mathcal{N}\left(0,\ \frac{2}{n_{\text{in}}}\right) $$

このように分散を大きめに設定することで、ReLU 関数と組み合わせた場合でも、勾配消失が起こりにくくなる。

1.4 バッチ正規化(Batch Normalization)

バッチ正規化は、各層への入力をミニバッチ単位で正規化する手法であり、学習の安定化を目的として用いられる。

ミニバッチ内の入力 $x$ に対して、平均と分散を用いて次のように正規化を行う。

$$ \hat{x} = \frac{x - \mu_B}{\sqrt{\sigma_B^2 + \varepsilon}} $$

ここで、$\mu_B$ と $\sigma_B^2$ はミニバッチ内の平均および分散であり、$\varepsilon$ は数値安定化のための微小値である。

さらに、学習可能なパラメータ $\gamma, \beta$ を用いて次の変換を行う。

$$ y = \gamma \hat{x} + \beta $$

これにより、入力分布の変動を抑えつつ、モデルの表現力を維持することができる。

実装演習

配布コードの実行

配布コード 2_2_2_vanishing_gradient_modified.ipynb の実行結果を載せる。

  • シグモイド関数 & ガウス分布初期化

stage_3_2_01.png

  • ReLU 関数 & ガウス分布初期化

stage_3_2_02.png

  • シグモイド関数 & Xavier 初期化

stage_3_2_03.png

  • ReLU 関数 & He 初期化

stage_3_2_04.png

[try] hidden_size_list の数字を変更してみよう

配布コードの「ReLU 関数 & He 初期化」を使って、隠れ層の数を増加させて観察する。

hidden_size_list を以下のように変化させた。

hidden_size_list = [40, 20] + [20]*4

stage_3_2_05.png

実行結果としては、accuracy は学習初期から急激に変化するのではなく、反復回数に応じて緩やかに向上した。

これは、He 初期化により各層での勾配のスケールが適切に保たれ、ReLU の特性と相まって学習が安定して進んだためと考えられる。

この結果は、勾配消失を抑制することで深層ネットワークの学習が安定化することを示している。

[try] sigmoid - He と relu - Xavier についても試してみよう

  • シグモイド関数 & He 初期化

stage_3_2_06.png

シグモイド関数と He 初期化を組み合わせた場合でも、学習は安定して進行し、一定の精度に到達した。

しかし、ReLU と He 初期化を用いた場合と比較すると、学習の立ち上がりが遅く、深層化に対する耐性という点では十分とは言えないと考えられる。

このことから、初期化手法と活性化関数の組み合わせが学習挙動に大きく影響することが分かる。

  • ReLU 関数 & Xavier 初期化

stage_3_2_07.png

ReLU 関数と Xavier 初期化を用いた場合、学習初期から accuracy が速やかに向上し、安定して高い精度に到達した。

これは、ReLU により活性化関数由来の勾配消失が抑制され、Xavier 初期化によって勾配の分散が極端に崩れなかったためと考えられる。

確認テスト

連鎖律の原理を使い、dz/dx を求めよ

$$ z = t^2,\quad t = x + y $$

$z$ は $t$ を通じて $x$ に依存しているため、連鎖律より

$$ \frac{dz}{dx} = \frac{dz}{dt} \cdot \frac{dt}{dx} $$

と書ける。

$$ \frac{dz}{dt} = 2t, \frac{dt}{dx} = 1 $$

より、

$$ \frac{dz}{dx} = 2t \cdot 1 = 2(x + y) $$

シグモイド関数を微分した時、入力値が 0 の時に最大値をとる。その値として正しいものを選択肢から選べ

シグモイド関数を

$$ \sigma(x)=\frac{1}{1+e^{-x}} $$

とすると、その微分は

$$ \sigma'(x)=\sigma(x)\bigl(1-\sigma(x)\bigr) $$

で与えられる。

入力値 $x=0$ のとき

$$ \sigma(0)=\frac{1}{2} $$

より、

$$ \sigma'(0)=\frac{1}{2}\left(1-\frac{1}{2}\right)=\frac{1}{4}=0.25 $$

したがって、正しい選択肢は(2)$0.25$ と計算できる。

重みの初期値に 0 を設定すると、どのような問題が発生するか。簡潔に説明せよ

重みの初期値をすべて 0 に設定すると、各ユニットが同じ値・同じ勾配を持つ対称な状態になり、学習によって役割分担が生じなくなる。

その結果、重みが同一に更新され続け、表現力が向上せず学習が進まないという問題が発生する。

一般的に考えられるバッチ正規化の効果を 2 点挙げよ

  1. 各層の出力分布を正規化することで勾配が安定し、学習が高速かつ安定に進む

  2. 学習中に入力分布の変動が抑えられるため、勾配消失や勾配爆発を起こしにくくなる

参考図書 / 関連記事

  • ゼロから作る Deep Learning - Pythonで学ぶディープラーニングの理論と実装(斎藤 康毅 著)
    • p.183 - 184

Xavier 初期化は、活性化関数が線形、あるいは原点付近で線形近似できることを前提として導かれた重み初期化手法である。

sigmoid 関数や tanh 関数は、中央付近では線形関数とみなせる領域を持ち、かつ左右対称な形状をしているため、Xavier 初期化との相性が良いとされている。

一方、ReLU 関数は非対称であり、負の入力を $0$ に切り捨てる特性を持つため、Xavier 初期化をそのまま用いると分散が適切に保たれない。

この問題に対応するため、ReLU 関数に特化して設計された初期化手法が He 初期化である。

また、sigmoid 関数と tanh 関数の違いとして、出力の中心がそれぞれ $(0, 0.5)$ と $(0, 0)$ に位置する点が挙げられる。

一般に、活性化関数が原点対称であることは、勾配の伝播を安定させる上で望ましい性質であることが知られており、この点において tanh 関数は sigmoid 関数よりも有利である。

2. 学習率最適化手法

勾配降下法では、学習率の設定が学習の収束速度や安定性に大きく影響する。

学習率が大きすぎると発散し、小さすぎると学習が極端に遅くなるため、学習率を適切に制御する最適化手法が提案されている。

以下では、代表的な学習率最適化手法について、その更新式を中心に整理する。

2.1 モメンタム(Momentum)

モメンタムは、過去の勾配情報を蓄積し、その慣性を利用して更新を行う手法である。

勾配降下法における振動を抑え、収束を高速化することを目的としている。

更新式は以下の通りである。

$$ \begin{aligned} \boldsymbol{v}_t &= \alpha \boldsymbol{v}_{t-1} - \eta \nabla E(\boldsymbol{w}_t) \\ \boldsymbol{w}_{t+1} &= \boldsymbol{w}_t + \boldsymbol{v}_t \end{aligned} $$

ここで、$\boldsymbol{v}_t$ は速度ベクトル、$\alpha$ はモメンタム係数、$\eta$ は学習率を表す。

2.2 AdaGrad

AdaGrad は、各パラメータごとに学習率を調整する手法であり、頻繁に更新されるパラメータの学習率を小さくする特徴を持つ。

更新式は以下の通りである。

$$ \begin{aligned} \boldsymbol{h}_t &= \boldsymbol{h}_{t-1} + (\nabla E(\boldsymbol{w}_t))^2 \\ \boldsymbol{w}_{t+1} &= \boldsymbol{w}_t - \frac{\eta}{\sqrt{\boldsymbol{h}_t} + \epsilon} \nabla E(\boldsymbol{w}_t) \end{aligned} $$

$\boldsymbol{h}_t$ は過去の勾配の二乗和を表し、$\epsilon$ はゼロ除算を防ぐための微小値である。

2.3 RMSProp

RMSProp は、AdaGrad において 学習が進むにつれて学習率が過度に小さくなる問題を改善した手法である。

過去の勾配の二乗を指数移動平均で保持する。

更新式は以下の通りである。

$$ \begin{aligned} \boldsymbol{h}_t &= \rho \boldsymbol{h}_{t-1} + (1-\rho)(\nabla E(\boldsymbol{w}_t))^2 \\ \boldsymbol{w}_{t+1} &= \boldsymbol{w}_t - \frac{\eta}{\sqrt{\boldsymbol{h}_t} + \epsilon} \nabla E(\boldsymbol{w}_t) \end{aligned} $$

ここで、$\rho$ は減衰率を表す。

2.4 Adam

Adam は、モメンタムと RMSProp を組み合わせた最適化手法であり、現在最も広く用いられている手法の一つである。

一次モーメント(平均)と二次モーメント(分散)の両方を考慮する。

更新式は以下の通りである。

$$ \begin{aligned} \boldsymbol{m}_t &= \beta_1 \boldsymbol{m}_{t-1} + (1-\beta_1)\nabla E(\boldsymbol{w}_t) \\ \boldsymbol{v}_t &= \beta_2 \boldsymbol{v}_{t-1} + (1-\beta_2)(\nabla E(\boldsymbol{w}_t))^2 \end{aligned} $$

バイアス補正を行った後、

$$ \boldsymbol{w}_{t+1} = \boldsymbol{w}_t - \frac{\eta}{\sqrt{\hat{\boldsymbol{v}}_t} + \epsilon}\hat{\boldsymbol{m}}_t $$

としてパラメータを更新する。

実装演習

配布コードの実行

配布コード 2_4_optimizer_after.ipynb の実行結果を載せる。

  • SGD

stage_3_2_08.png

  • Momentum

stage_3_2_09.png

  • AdaGrad

stage_3_2_10.png

  • RMSProp

stage_3_2_11.png

  • Adam

stage_3_2_12.png

[try] 活性化関数と重みの初期化方法を変更して違いを見てみよう

import numpy as np
import matplotlib.pyplot as plt
from data.mnist import load_mnist
from multi_layer_net import MultiLayerNet

##################################################
#  共通設定(必要なら変更)
##################################################
hidden_size_list = [40, 20]
iters_num = 1000
batch_size = 100
learning_rate = 0.01
plot_interval = 10
use_batchnorm = False
seed = 0

##################################################
#  データ読み込み(1回だけ)
##################################################
(x_train, d_train), (x_test, d_test) = load_mnist(normalize=True, one_hot_label=True)
train_size = x_train.shape[0]
print("データ読み込み完了")

##################################################
#  1条件を学習して精度推移を返す
##################################################
def run_sgd_experiment(
    activation: str,
    weight_init_std,
    *,
    hidden_size_list,
    iters_num, batch_size,
    learning_rate,
    plot_interval,
    use_batchnorm,
    seed,
):
    np.random.seed(seed)

    network = MultiLayerNet(
        input_size=784,
        hidden_size_list=hidden_size_list,
        output_size=10,
        activation=activation,
        weight_init_std=weight_init_std,
        use_batchnorm=use_batchnorm
    )

    accuracies_train = []
    accuracies_test = []

    for i in range(iters_num):
        batch_mask = np.random.choice(train_size, batch_size)
        x_batch = x_train[batch_mask]
        d_batch = d_train[batch_mask]

        grad = network.gradient(x_batch, d_batch)

        # paramsのキーはネットワーク構造に依存するので、存在するものを全部更新する
        for key in network.params.keys():
            network.params[key] -= learning_rate * grad[key]

        if (i + 1) % plot_interval == 0:
            accr_test = network.accuracy(x_test, d_test)
            accr_train = network.accuracy(x_batch, d_batch)
            accuracies_test.append(accr_test)
            accuracies_train.append(accr_train)

    return accuracies_train, accuracies_test

##################################################
#  6条件を回す(2×3)
##################################################
# initの指定を MultiLayerNet が受け取る形に合わせる
# gauss: 数値(例 0.01)
# Xavier / He: 文字列
init_map = {
    "gauss": 0.01,
    "xavier": "Xavier",
    "he": "He",
}

act_list = ["sigmoid", "relu"]
init_list = ["gauss", "xavier", "he"]

results = {}

for act in act_list:
    for init_name in init_list:
        w_init = init_map[init_name]
        print(f"Running: {act} × {init_name} (weight_init_std={w_init})")
        tr, te = run_sgd_experiment(
            activation=act,
            weight_init_std=w_init,
            hidden_size_list=hidden_size_list,
            iters_num=iters_num,
            batch_size=batch_size,
            learning_rate=learning_rate,
            plot_interval=plot_interval,
            use_batchnorm=use_batchnorm,
            seed=seed
        )
        results[(act, init_name)] = (tr, te)

##################################################
#  2行×3列で描画
##################################################
fig, axes = plt.subplots(2, 3, figsize=(16, 8))
fig.suptitle("Activation × Weight Initialization (SGD)", fontsize=16)

x = list(range(plot_interval, iters_num + 1, plot_interval))

for r, act in enumerate(act_list):
    for c, init_name in enumerate(init_list):
        ax = axes[r, c]
        tr, te = results[(act, init_name)]

        ax.plot(x, tr, label="train")
        ax.plot(x, te, label="test")
        ax.set_title(f"{act} × {init_name}")
        ax.set_xlabel("iteration")
        ax.set_ylabel("accuracy")
        ax.set_ylim(0, 1.0)
        ax.legend(loc="lower right")

plt.tight_layout()
plt.show()

stage_3_2_13.png

活性化関数と重み初期化の組み合わせによる学習挙動を比較した結果、ReLU 関数と He 初期化の組み合わせが最も安定して高い精度に到達した。

一方、sigmoid 関数とガウス初期化では学習がほとんど進まず、 活性化関数と初期化手法の適切な組み合わせが重要であることが確認できた。

[try] バッチ正規化をして変化を見てみよう

前項のコードを使用して、use_batchnorm = True で実行する。

stage_3_2_14.png

バッチ正規化なしのときに学習が停滞していた条件(特に sigmoid × gaussrelu × gauss)でも accuracy が大きく改善し、学習が安定して進むようになった。

これは、各層の出力(中間表現)をミニバッチ単位で正規化し、平均・分散を揃えることで、層をまたいだ信号のスケール変動が抑えられたためと考えられる。

その結果、勾配の大きさが極端に小さくなる(勾配消失)あるいは不安定になる状況が緩和され、学習が進みやすくなった。

また、バッチ正規化を用いることで、活性化関数や重み初期化に対する依存度が低下している点も確認できる。

バッチ正規化なしでは「ReLU × He」など特定の組み合わせが顕著に優位であったが、バッチ正規化ありでは多くの条件で高い精度に到達しており、初期化の差が相対的に小さくなっている。

以上より、バッチ正規化は学習の安定化と収束の改善に寄与し、重み初期化や活性化関数選択の難しさを一定程度緩和する手法であることが分かった。

確認テスト

モメンタム・AdaGrad・RMSProp の特徴をそれぞれ簡潔に説明せよ

  • モメンタム(Momentum)

過去の勾配の移動平均を用いて更新方向に慣性を持たせることで、振動を抑えつつ一貫した方向へ学習を進め、収束を高速化する手法である。

  • AdaGrad

各パラメータごとに過去の勾配の二乗和を蓄積し、頻繁に更新されるパラメータほど学習率を小さくすることで、学習率を自動調整する手法である。

  • RMSProp

AdaGrad で学習率が極端に小さくなる問題を緩和するため、勾配の二乗の移動平均を用いて学習率を調整し、長期学習でも安定した更新を可能にする手法である。

参考図書 / 関連記事

  • ゼロから作る Deep Learning - Pythonで学ぶディープラーニングの理論と実装(斎藤 康毅 著)
    • p.170 - 177

最適化の更新経路を図示していたので、コードを書いて図示して比較する。

import numpy as np
import matplotlib.pyplot as plt

##################################################
#  目的関数(細長い谷)と勾配
#    f(x, y) = x^2/20 + y^2
##################################################
def f(xy):
    x, y = xy
    return (x ** 2) / 20.0 + (y ** 2)

def grad_f(xy):
    x, y = xy
    return np.array([x / 10.0, 2.0 * y])

##################################################
#  Optimizers
##################################################
class SGD:
    def __init__(self, lr=0.95):
        self.lr = lr
    def update(self, xy, g):
        return xy - self.lr * g

class Momentum:
    def __init__(self, lr=0.1, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = np.zeros(2)
    def update(self, xy, g):
        self.v = self.momentum * self.v - self.lr * g
        return xy + self.v

class AdaGrad:
    def __init__(self, lr=1.5, eps=1e-7):
        self.lr = lr
        self.eps = eps
        self.h = np.zeros(2)
    def update(self, xy, g):
        self.h += g * g
        return xy - self.lr * g / (np.sqrt(self.h) + self.eps)

class RMSProp:
    def __init__(self, lr=0.1, decay=0.9, eps=1e-7):
        self.lr = lr
        self.decay = decay
        self.eps = eps
        self.h = np.zeros(2)
    def update(self, xy, g):
        self.h = self.decay * self.h + (1.0 - self.decay) * (g * g)
        return xy - self.lr * g / (np.sqrt(self.h) + self.eps)

class Adam:
    def __init__(self, lr=0.3, beta1=0.9, beta2=0.999, eps=1e-7):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.eps = eps
        self.m = np.zeros(2)
        self.v = np.zeros(2)
        self.t = 0

    def update(self, xy, g):
        self.t += 1
        self.m = self.beta1 * self.m + (1 - self.beta1) * g
        self.v = self.beta2 * self.v + (1 - self.beta2) * (g * g)

        m_hat = self.m / (1 - self.beta1 ** self.t)
        v_hat = self.v / (1 - self.beta2 ** self.t)

        return xy - self.lr * m_hat / (np.sqrt(v_hat) + self.eps)

##################################################
#  軌跡生成
##################################################
def run(opt, xy0=np.array([-7.0, 2.0]), steps=30):
    xy = xy0.astype(float).copy()
    traj = [xy.copy()]
    for _ in range(steps):
        g = grad_f(xy)
        xy = opt.update(xy, g)
        traj.append(xy.copy())
    return np.array(traj)

##################################################
#  描画
##################################################
optimizers = {
    "SGD": SGD(lr=0.95),
    "Momentum": Momentum(lr=0.1, momentum=0.9),
    "AdaGrad": AdaGrad(lr=1.5),
    "RMSProp": RMSProp(lr=0.1),
    "Adam": Adam(lr=0.3),
}

x = np.linspace(-10, 10, 400)
y = np.linspace(-10, 10, 400)
X, Y = np.meshgrid(x, y)
Z = (X**2)/20.0 + (Y**2)

fig, axes = plt.subplots(2, 3, figsize=(15, 10))
axes = axes.ravel()

for ax, (name, opt) in zip(axes, optimizers.items()):
    ax.contour(X, Y, Z, levels=np.logspace(-1, 2, 20), linewidths=1)
    traj = run(opt)
    ax.plot(traj[:, 0], traj[:, 1], marker="o", linewidth=1.5)
    ax.set_title(name)
    ax.set_xlim(-10, 10)
    ax.set_ylim(-10, 10)
    ax.set_xlabel("x")
    ax.set_ylabel("y")
    ax.grid(True, alpha=0.3)

# 余った1枠を非表示
for i in range(len(optimizers), len(axes)):
    axes[i].axis("off")

fig.suptitle("Optimization trajectories (SGD / Momentum / AdaGrad / RMSProp / Adam)", fontsize=14)
plt.tight_layout()
plt.show()

stage_3_2_15.png

細長い谷形状の目的関数に対して、以下のようなことが分かる。

  • SGD は、勾配方向に対してジグザグに進むので非効率
  • Momentum は、慣性により振動が抑制され、ジグザグ度合いが軽減されている
  • AdaGradRMSProp は、学習率を自動調整することで安定した収束を示す
  • Adam は、それらを統合した手法として,初期段階から高速かつ安定した最適化経路を描く

3. 過学習

深層学習モデルは表現力が高く、訓練データに対して誤差を小さくすることができる一方で、訓練データに過剰に適合してしまい、未知データで性能が落ちることがある。この現象を 過学習(overfitting) という。これを抑えて、汎化性能を向上させることが正則化の目的である。

深層学習における正則化は、大きく 陽的正則化(explicit regularization)陰的正則化(implicit regularization) に分類される。

陽的正則化は損失関数に正則化項を加える方法であり、陰的正則化は学習の進め方(ハイパーパラメータ設定など)そのものが正則化として働く方法である。

3.1 Weight decay と L1/L2 正則化(陽的正則化)

過学習の一因は、モデルが訓練データに合わせて重みを過度に大きくし、複雑な関数(高分散なモデル)を作ってしまうことである。そこで、重みの大きさにペナルティを課すことで、モデルの複雑さを抑える。

L2 正則化(Weight decay)

L2 正則化は、損失関数 $E(\boldsymbol{w})$ に対して重みの二乗和を加える。

$$ E_{\text{reg}}(\boldsymbol{w}) = E(\boldsymbol{w}) + \frac{\lambda}{2}|\boldsymbol{w}|_2^2 = E(\boldsymbol{w}) + \frac{\lambda}{2}\sum_i w_i^2 $$

このとき勾配は

$$ \nabla E_{\text{reg}}(\boldsymbol{w}) = \nabla E(\boldsymbol{w}) + \lambda \boldsymbol{w} $$

となり、勾配降下法による更新は

$$ \boldsymbol{w} \leftarrow \boldsymbol{w} - \eta\left(\nabla E(\boldsymbol{w}) + \lambda \boldsymbol{w}\right) $$

で与えられる。

右辺の $-\eta\lambda \boldsymbol{w}$ が 重みを原点方向へ縮める(減衰させる) ため、L2 正則化は Weight decay と呼ばれる。

これにより、重みが過度に大きくなることが抑えられ、過学習の抑制が期待できる。

L1 正則化

L1 正則化は、損失関数に重みの絶対値和を加える。

$$ E_{\text{reg}}(\boldsymbol{w}) = E(\boldsymbol{w}) + \lambda|\boldsymbol{w}|_1 = E(\boldsymbol{w}) + \lambda\sum_i |w_i| $$

L1 は多くの重みを ちょうど 0 にしやすい(疎性) ため、不要な特徴(結合)を削ってモデルを単純化する方向に働く。

一方、L2 は重みを連続的に小さくし、全体的に滑らかなモデルになりやすい。

3.2 ドロップアウト

ドロップアウトは、学習時に ランダムにノードを削除して学習させる 手法であり、過学習を抑制して汎化性能を向上させる目的で用いられる。陰的正則化の一種である。

各ユニットの出力を確率的に無効化することで、特定ユニットへの依存(共適応)を防ぎ、アンサンブル学習に近い効果を得られる。

学習時のマスク $\boldsymbol{m}$(要素が 0/1)を用いて

$$ \boldsymbol{h}' = \boldsymbol{m}\odot \boldsymbol{h} $$

のように中間層出力 $\boldsymbol{h}$ を部分的に落とす。

推論時は、学習時の期待値と整合するようにスケーリングする(実装流儀により、学習時にスケールする “inverted dropout” を使うことが多い)。

3.3 ドロップコネクト

ドロップコネクトは、ドロップアウトが「ノード(ユニット)を落とす」のに対し、結合(重み)をランダムに落とす 発想の正則化である。陰的正則化の一種である。

形式的には、重み行列 $W$ に対してマスク $M$ を掛け、

$$ \boldsymbol{u} = \boldsymbol{x}(M\odot W) + \boldsymbol{b} $$

のように、学習時だけ結合を確率的に無効化する。

ユニット単位で落とすより粒度が細かく、より強いランダム化として働く場合がある。

3.4 陰的正則化(エポック数・バッチサイズ・学習率)

陰的正則化は、「モデルの損失関数に正則化項を追加する」のではなく、「学習の進め方を決めるパラメータ調整が正則化の役割を果たす」ものであり、代表パラメータとして エポック数・バッチサイズ・学習率 が挙げられている。

  • エポック数

訓練データ全てを入力として学習を行う回数。

増やしすぎると訓練誤差は下がり続ける一方で、検証誤差が上がり始める(過学習)ため、学習曲線を見て適切な回数で止める必要がある。

また、検証性能が改善しなくなった時点で学習を終了する 早期終了(アーリーストッピング) は、エポック数を通じた代表的な陰的正則化である。

  • バッチサイズ

ミニバッチ学習における 1 バッチのデータ数。

バッチサイズが大きい設定では、大域最適解に収束しにくくなる場合がある。一般に、バッチサイズは勾配推定のノイズ量を変化させ、学習の安定性・汎化に影響する。

  • 学習率

重み更新のステップ幅。

学習率が大きすぎると最適解近傍で振動や発散が起こり、小さすぎると学習が進まない。学習率の大きさにより大域最適解へ収束しにくくなる場合がある。

実装演習

配布コードの実行

配布コード 2_5_overfiting.ipynb の実行結果を載せる。

  • 過学習

stage_3_2_16.png

  • Weight decay (L2)

stage_3_2_17.png

  • Weight decay (L1)

stage_3_2_18.png

  • ドロップアウト

stage_3_2_19.png

  • ドロップアウト + L1

stage_3_2_20.png

[try] weigth_decay_lambda の値を変更して正則化の強さを確認しよう

Weight decay (L2) で確認する。

配布コードでは weight_decay_lambda = 0.1 となっているので、少し増やして weight_decay_lambda = 0.15 で実行する。

stage_3_2_21.png

train / test の accuracy がさらに低下した。

これは正則化が強くなりすぎ、重みが過度に抑制された結果、モデルが十分な表現を学習できなくなったためと考えられる。

この結果から、Weight decay には過学習を抑制しつつ性能を保つ、適切な強さの範囲が存在することが分かる。

[try] dropout_ratio の値を変更してみよう

配布コードでは dropout_ratio = 0.15 となっているので、少し増やして dropout_ratio = 0.2 で実行する。

stage_3_2_22.png

train / test の accuracy が、全体として低めに推移した。

これはドロップアウトがやや強く働き、学習時に有効なユニット数が減少したことで、特徴表現の学習が十分に進まなかったためと考えられる。

これより、ドロップアウトは過学習を抑制する一方で、強くしすぎると学習そのものを阻害することが確認できた。

[try] optimizer と dropout_ratio の値を変更してみよう

4 種類の optimizer を比較する。

stage_3_2_23.png

ドロップアウト(dropout_ratio=0.15)を用いた場合でも、optimizer によって収束速度と汎化性能に明確な差が見られた。

SGD は収束が遅く test accuracy も低めである一方、Momentum・AdaGrad・Adam は初期から高速に収束し、test accuracy も高く安定した。

特に Adam は学習初期から安定した性能を示し、ドロップアウト下でも最も効率よく学習できることが確認できた。

確認テスト

下図について、L1 正則化を表しているグラフはどちらか答えよ

右側。

理由として、L1 正則化(Lasso) では制約領域がひし形(ダイヤモンド形)になり、等高線との接点が軸上に来やすいため、重みがちょうど 0 になる スパース解 が得られます。

一方、左側の円形の制約は L2 正則化(Ridge) を表しています。

参考図書 / 関連記事

  • ゼロから作る Deep Learning - Pythonで学ぶディープラーニングの理論と実装(斎藤 康毅 著)
    • p.197

アンサンブル学習 とは、複数のモデルを個別に学習させ、推論時にそれらの出力を平均や多数決などで統合することで、汎化性能を向上させる手法である。

単一モデルに比べて予測のばらつきが抑えられ、過学習を起こしにくくなることが知られている。

ニューラルネットワークにおける Dropout は、このアンサンブル学習と密接な関係を持つ。Dropout は学習時にニューロンをランダムに無効化することで、毎回異なるネットワーク構造を学習していると解釈できる。

推論時には、Dropout によって無効化された割合を考慮して出力をスケーリングすることで、これら多数のネットワークの平均を近似的に取っている。

このように、Dropout は アンサンブル学習の効果を単一のネットワークで効率的に実現する正則化手法 であり、過学習の抑制と汎化性能の向上に寄与する。

4. 畳み込みニューラルネットワークの概念

全体像

畳み込みニューラルネットワーク(Convolutional Neural Network : CNN) は、主に画像認識などの空間構造を持つデータを扱うために設計された深層学習モデルである。

全結合層(Fully Connected Layer)では入力の位置関係が失われるのに対し、CNN では 空間的な局所構造を保ったまま特徴を抽出できる という特徴を持つ。

CNN は主に以下の層から構成される。

  • 畳み込み層(Convolution Layer)
  • プーリング層(Pooling Layer)
  • (最終段で)全結合層

畳み込み層とプーリング層を交互に重ねることで、画像の局所的特徴から抽象的特徴へと段階的に表現を学習する。

4.1 畳み込み層(Convolution Layer)

畳み込み層は、入力データに対して フィルタ(カーネル) と呼ばれる小さな重み行列を滑らせながら積和演算を行い、特徴マップ(feature map)を生成する層である。

入力が

  • 高さ $H$
  • 幅 $W$
  • チャンネル数 $C$

を持つ場合、入力形状は $H \times W \times C$ と表される。

フィルタは入力の局所領域にのみ接続されるため、全結合層と比べて パラメータ数が大幅に削減される と同時に、局所的な特徴(エッジや模様など)を効率よく捉えることができる。

出力サイズの計算

畳み込み層では、入力サイズ・フィルタサイズ・パディング・ストライドの関係から、出力される特徴マップの空間サイズが決まる。

入力の高さを $H$、幅を $W$、フィルタの高さを $F_H$、幅を $F_W$、パディングを $P$、ストライドを $S$ とすると、出力特徴マップの高さ $O_H$、幅 $O_W$ は次式で与えられる。

$$ O_H = \frac{H + 2P - F_H}{S} + 1 $$

$$ O_W = \frac{W + 2P - F_W}{S} + 1 $$

この式から分かるように、

  • パディング $P$ を大きくすると出力サイズは大きくなる
  • ストライド $S$ を大きくすると出力サイズは小さくなる
  • フィルタサイズ $F_H, F_W$ が大きいほど出力は小さくなる

といった関係がある。

特に、

$$ P = \frac{F_H - 1}{2}, \quad S = 1 $$

と設定すると、出力サイズが入力サイズと等しくなり(same padding)、深い畳み込みネットワークでも空間サイズを保ったまま学習を進めることができる。

4.1.1 バイアス

畳み込み層では、各フィルタごとに 1 つのバイアス が用意される。

畳み込み演算によって得られた値にバイアスを加えた後、活性化関数が適用される。

$$ \text{output} = \text{convolution}(X, W) + b $$

このバイアスにより、出力の全体的なシフトが可能となり、表現力が向上する。

4.1.2 パディング

パディングとは、入力データの周囲に 値(通常は 0)を追加する処理 である。

パディングを行わない場合、畳み込みを重ねるごとに特徴マップのサイズは小さくなる。

パディングを用いることで、

  • 出力サイズを入力と同じに保つ
  • 画像の端の情報を十分に利用する

といった効果が得られる。

特に、入力と出力の空間サイズを同一に保つ same padding は、深い CNN でよく用いられる。

4.1.3 ストライド

ストライドとは、フィルタを どれだけの間隔で移動させるか を表すパラメータである。

  • $S=1$:1 ピクセルずつずらして計算する(最も細かい)
  • $S=2$:2 ピクセルずつ飛ばして計算する(出力が間引かれる)
  • $S=3$:3 ピクセルずつ…(さらに粗くなる)

ストライドを大きくすると、出力サイズは小さくなり、特徴マップのダウンサンプリング効果 を持つ。

4.1.4 チャンネル

入力データがカラー画像の場合、通常は

  • 赤(R)
  • 緑(G)
  • 青(B)

の 3 チャンネルを持つ。

畳み込み層のフィルタは、入力の全チャンネルにまたがる形で定義される。

例えば、入力が $H \times W \times 3$ の場合、フィルタは $kH \times kW \times 3$ の形状を持つ。

また、フィルタの個数 = 出力チャンネル数 であり、複数のフィルタを用いることで、異なる種類の特徴を同時に抽出できる。

4.2 プーリング層

プーリング層は、特徴マップの空間サイズを縮小する層であり、主に Max Pooling が用いられる。

例えば、$2 \times 2$ の領域から最大値を取り出すことで、

  • 特徴マップのサイズ削減
  • 計算量の削減
  • 小さな位置ずれに対する頑健性の向上

といった効果が得られる。

プーリング層には学習すべきパラメータは存在せず、畳み込み層で抽出された特徴を整理・要約する役割を担う。

実装演習

基本的な CNN

配布コード 2_6_simple_convolution_network_after.ipynb の実行結果を載せる。

stage_3_2_24.png

Simple Convolution Network を用いた学習では、学習初期から訓練精度・テスト精度ともに急速に向上し、最終的に高い認識精度に到達した。これは、畳み込み層によって画像の局所的特徴を効率よく抽出できたためであり、全結合層のみのネットワークと比べて学習が安定したと考えられる。

また、訓練精度がほぼ 1.0 に近づく一方で、テスト精度も高い値を維持しており、過度な過学習は見られなかった。これは、畳み込み層とプーリング層によるパラメータ削減と空間情報の集約が、汎化性能の向上に寄与していることを示している。

以上より、Simple Convolution Network であっても、画像認識タスクにおいては十分に高い性能を発揮でき、CNN の有効性を確認することができた。

[try] im2col の処理を確認しよう

  • 関数内で transpose の処理をしている行をコメントアウトして下のコードを実行してみよう

確認用コード。

import numpy as np

def show_im2col(input_shape, filter_shape, stride, pad):
    N, C, H, W = input_shape
    FH, FW = filter_shape

    x = np.arange(N*C*H*W).reshape(N, C, H, W)
    col = im2col(x, FH, FW, stride=stride, pad=pad)

    OH = (H + 2*pad - FH)//stride + 1
    OW = (W + 2*pad - FW)//stride + 1

    print()
    print("x.shape =", x.shape)
    print("FH,FW =", (FH, FW), " stride =", stride, " pad =", pad)
    print("OH,OW =", (OH, OW))
    print("col.shape =", col.shape)  # 期待: (N*OH*OW, C*FH*FW)
    print("col (先頭5行)\n", col[:5])

# ケースA
show_im2col((1,1,4,4), (2,2), stride=1, pad=0)

# ケースB
show_im2col((1,1,4,4), (2,2), stride=1, pad=1)

# ケースC
show_im2col((1,1,6,6), (3,3), stride=2, pad=0)

transpose あり。


x.shape = (1, 1, 4, 4)
FH,FW = (2, 2)  stride = 1  pad = 0
OH,OW = (3, 3)
col.shape = (9, 4)
col (先頭5行)
 [[ 0.  1.  4.  5.]
 [ 1.  2.  5.  6.]
 [ 2.  3.  6.  7.]
 [ 4.  5.  8.  9.]
 [ 5.  6.  9. 10.]]

x.shape = (1, 1, 4, 4)
FH,FW = (2, 2)  stride = 1  pad = 1
OH,OW = (5, 5)
col.shape = (25, 4)
col (先頭5行)
 [[0. 0. 0. 0.]
 [0. 0. 0. 1.]
 [0. 0. 1. 2.]
 [0. 0. 2. 3.]
 [0. 0. 3. 0.]]

x.shape = (1, 1, 6, 6)
FH,FW = (3, 3)  stride = 2  pad = 0
OH,OW = (2, 2)
col.shape = (4, 9)
col (先頭5行)
 [[ 0.  1.  2.  6.  7.  8. 12. 13. 14.]
 [ 2.  3.  4.  8.  9. 10. 14. 15. 16.]
 [12. 13. 14. 18. 19. 20. 24. 25. 26.]
 [14. 15. 16. 20. 21. 22. 26. 27. 28.]]

transpose なし。

x.shape = (1, 1, 4, 4)
FH,FW = (2, 2)  stride = 1  pad = 0
OH,OW = (3, 3)
col.shape = (9, 4)
col (先頭5行)
 [[ 0.  1.  2.  4.]
 [ 5.  6.  8.  9.]
 [10.  1.  2.  3.]
 [ 5.  6.  7.  9.]
 [10. 11.  4.  5.]]

x.shape = (1, 1, 4, 4)
FH,FW = (2, 2)  stride = 1  pad = 1
OH,OW = (5, 5)
col.shape = (25, 4)
col (先頭5行)
 [[ 0.  0.  0.  0.]
 [ 0.  0.  0.  1.]
 [ 2.  3.  0.  4.]
 [ 5.  6.  7.  0.]
 [ 8.  9. 10. 11.]]

x.shape = (1, 1, 6, 6)
FH,FW = (3, 3)  stride = 2  pad = 0
OH,OW = (2, 2)
col.shape = (4, 9)
col (先頭5行)
 [[ 0.  2. 12. 14.  1.  3. 13. 15.  2.]
 [ 4. 14. 16.  6.  8. 18. 20.  7.  9.]
 [19. 21.  8. 10. 20. 22. 12. 14. 24.]
 [26. 13. 15. 25. 27. 14. 16. 26. 28.]]

コメントアウト前後で col.shape は同じだが、各行に並ぶ要素の順序が異なっている。

transpose を行うことで、各行が「同一位置の受容野(C×FH×FW)」を表すように並び替えられ、畳み込みを行列積として正しく計算できる。

transpose を外すと並びの意味が崩れ、後段の行列積が畳み込みとして成立しなくなる。

  • input_data の各次元のサイズやフィルターサイズ・ストライド・パディングを変えてみよう

確認コード。

import numpy as np

def run_case(input_shape, filter_shape, stride, pad):
    N, C, H, W = input_shape
    FH, FW = filter_shape

    # 中身が分かりやすい連番入力
    x = np.arange(N * C * H * W).reshape(N, C, H, W)

    col = im2col(x, FH, FW, stride=stride, pad=pad)

    OH = (H + 2 * pad - FH) // stride + 1
    OW = (W + 2 * pad - FW) // stride + 1

    print("===================================")
    print(f"x.shape = {x.shape}")
    print(f"FH,FW = {filter_shape}, stride = {stride}, pad = {pad}")
    print(f"OH,OW = ({OH}, {OW})")
    print(f"col.shape = {col.shape}")  # (N*OH*OW, C*FH*FW)
    print("col (先頭5行)")
    print(col[:5])
    print()


print("ケース1:基本(基準)")
run_case(
    input_shape=(1, 1, 4, 4),
    filter_shape=(2, 2),
    stride=1,
    pad=0
)

print("ケース2:padding の効果")
run_case(
    input_shape=(1, 1, 4, 4),
    filter_shape=(2, 2),
    stride=1,
    pad=1
)

print("ケース3:stride の効果")
run_case(
    input_shape=(1, 1, 6, 6),
    filter_shape=(3, 3),
    stride=2,
    pad=0
)

print("ケース4:フィルタサイズの効果")
run_case(
    input_shape=(1, 1, 6, 6),
    filter_shape=(4, 4),
    stride=1,
    pad=0
)

実行結果。

ケース1:基本(基準)
===================================
x.shape = (1, 1, 4, 4)
FH,FW = (2, 2), stride = 1, pad = 0
OH,OW = (3, 3)
col.shape = (9, 4)
col (先頭5行)
[[ 0.  1.  4.  5.]
 [ 1.  2.  5.  6.]
 [ 2.  3.  6.  7.]
 [ 4.  5.  8.  9.]
 [ 5.  6.  9. 10.]]

ケース2:padding の効果
===================================
x.shape = (1, 1, 4, 4)
FH,FW = (2, 2), stride = 1, pad = 1
OH,OW = (5, 5)
col.shape = (25, 4)
col (先頭5行)
[[0. 0. 0. 0.]
 [0. 0. 0. 1.]
 [0. 0. 1. 2.]
 [0. 0. 2. 3.]
 [0. 0. 3. 0.]]

ケース3:stride の効果
===================================
x.shape = (1, 1, 6, 6)
FH,FW = (3, 3), stride = 2, pad = 0
OH,OW = (2, 2)
col.shape = (4, 9)
col (先頭5行)
[[ 0.  1.  2.  6.  7.  8. 12. 13. 14.]
 [ 2.  3.  4.  8.  9. 10. 14. 15. 16.]
 [12. 13. 14. 18. 19. 20. 24. 25. 26.]
 [14. 15. 16. 20. 21. 22. 26. 27. 28.]]

ケース4:フィルタサイズの効果
===================================
x.shape = (1, 1, 6, 6)
FH,FW = (4, 4), stride = 1, pad = 0
OH,OW = (3, 3)
col.shape = (9, 16)
col (先頭5行)
[[ 0.  1.  2.  3.  6.  7.  8.  9. 12. 13. 14. 15. 18. 19. 20. 21.]
 [ 1.  2.  3.  4.  7.  8.  9. 10. 13. 14. 15. 16. 19. 20. 21. 22.]
 [ 2.  3.  4.  5.  8.  9. 10. 11. 14. 15. 16. 17. 20. 21. 22. 23.]
 [ 6.  7.  8.  9. 12. 13. 14. 15. 18. 19. 20. 21. 24. 25. 26. 27.]
 [ 7.  8.  9. 10. 13. 14. 15. 16. 19. 20. 21. 22. 25. 26. 27. 28.]]

入力サイズ、フィルタサイズ、ストライド、パディングを変更すると、出力サイズ(OH,OW)および im2col によって展開される行列の形状が変化することが確認できた。

特に、パディングは周辺情報の保持に、ストライドは空間的な間引きに対応しており、これらの組み合わせによって畳み込み層の振る舞いが制御されている。

[try] col2imの処理を確認しよう

  • im2col の確認で出力した col を image に変換して確認しよう

確認コード。

import numpy as np

##################################################
#  入力データの準備
##################################################
N, C, H, W = 1, 1, 4, 4
x = np.arange(N * C * H * W).reshape(N, C, H, W)

FH, FW = 2, 2
stride = 1
pad = 0

print("x (input image)")
print(x[0, 0])
print()


##################################################
#  im2col
##################################################
col = im2col(x, FH, FW, stride=stride, pad=pad)

print("col.shape =", col.shape)
print("col (先頭5行)")
print(col[:5])
print()


##################################################
#  col2im
##################################################
x_rec = col2im(col, x.shape, FH, FW, stride=stride, pad=pad)

print("x_rec (col2im result)")
print(x_rec[0, 0])
print()


##################################################
#  重なり回数の確認
##################################################
x_ones = np.ones_like(x)
col_ones = im2col(x_ones, FH, FW, stride=stride, pad=pad)
cnt = col2im(col_ones, x.shape, FH, FW, stride=stride, pad=pad)

print("overlap count map")
print(cnt[0, 0])

実行結果。

stage_3_2_25.png

col2im により col を画像形状へ戻すと、元の入力と一致しない結果が得られた。

これは、畳み込みにおいて受容野が重なる部分が加算されて復元されるためである。

col2im は im2col の単純な逆変換ではなく、逆伝播計算などで必要な加算処理を含む復元であることが確認できた。

確認テスト

サイズ 6×6 の入力画像を、サイズ 2×2 のフィルタで畳み込んだ時の出力画像のサイズを答えよ。なお、ストライドとパディングは 1 とする。

出力画像のサイズは 7 × 7

畳み込み層の出力サイズは

$$ O = \frac{H + 2P - F}{S} + 1 $$

で求められる。

ここで

  • 入力サイズ $H = 6$
  • フィルタサイズ $F = 2$
  • パディング $P = 1$
  • ストライド $S = 1$

を代入すると、

$$ O = \frac{6 + 2\times1 - 2}{1} + 1 = 7 $$

となる。

参考図書 / 関連記事

  • ゼロから作る Deep Learning - Pythonで学ぶディープラーニングの理論と実装(斎藤 康毅 著)
    • p.207

なぜ、全結合層ではなく、畳み込みニューラルネットワークを用いるのか。

全結合層(Affine 層)を用いたニューラルネットワークでは、隣接する層のすべてのニューロンが結合されるため、入力データの形状を考慮せずに処理が行われる。

画像データの場合、本来は「縦・横・チャンネル」という 3 次元の構造を持っているが、全結合層に入力する際にはこれを 1 次元に並べ替える必要があり、空間的な位置関係が失われてしまう

しかし画像には、近接するピクセル同士が似た値を持つ、チャンネル間に相関があるなど、形状に基づく重要な情報 が含まれている。全結合層では、これらの局所的な構造や距離の概念を捉えることができず、効率的な特徴抽出が難しいという問題がある。

一方、畳み込み層では入力の形状を保ったまま処理を行い、局所領域に対して同じフィルタを適用することで特徴を抽出する。そのため、空間的な構造を活かした学習が可能となり、パラメータ数も全結合層に比べて大幅に削減できる。

このように、畳み込みニューラルネットワークは、画像の持つ構造的特性を適切に扱える点で全結合層よりも優れており、画像認識などのタスクにおいて高い性能を発揮する。

5. 最新の CNN

近年の畳み込みニューラルネットワーク(CNN)は、単に層を深くするだけでなく、過学習の抑制や学習の安定化を重視した設計が行われている。

代表的なモデルである AlexNet では、畳み込み層とプーリング層を重ねる基本構造は LeNet と大きく変わらないものの、活性化関数として ReLU を採用し、全結合層に Dropout を導入することで深いネットワークの学習を可能にした。

このように、最新の CNN は構造そのものの革新というよりも、正則化手法や学習を安定させる工夫を取り入れることで性能を向上させており、実用的な深層モデルへと発展している。

参考図書 / 関連記事

  • ゼロから作る Deep Learning - Pythonで学ぶディープラーニングの理論と実装(斎藤 康毅 著)
    • p.237 - 238

本書では、LeNet から AlexNet に至る CNN の発展について解説されており、両者のネットワーク構造には本質的な大きな違いがないことが示されている。

一方で、AlexNet では ReLU の導入や Dropout、LRN などの手法が用いられ、より深いネットワークの学習が可能となった点が強調されている。

また、CNN の発展には大量のデータと GPU による高速な並列計算環境の普及が大きく寄与したことが述べられている。

6. [フレームワーク演習] 正則化/最適化

ニューラルネットワークは高い表現能力を持つ一方で、モデルが複雑になりすぎると訓練データに過剰に適合し、汎化性能が低下する問題が生じる。

このような過学習や、学習が不安定になる問題を抑制するために、正則化や正規化といった手法が用いられる。

6.1 過学習が起きる理由

過学習とは、訓練データに対する誤差は小さいにもかかわらず、未知のデータに対する誤差が大きくなる現象である。

学習の進行に伴い、訓練誤差は減少し続けるが、テスト誤差が途中から増加する場合、モデルは訓練データに特化した学習を行っていると考えられる。

過学習が起きる主な原因として、以下が挙げられる。

  • パラメータ数やノード数が多く、モデルが複雑すぎる
  • パラメータの値が特定の方向に偏っている
  • 学習データが不足している、あるいは偏っている

これらはいずれも、モデルの自由度が過剰であることに起因している。

6.2 正則化とは

正則化(Regularization)とは、ネットワークの自由度(層数、ノード数、パラメータの値など)に制約を与え、モデルの複雑さを抑制することで、訓練データへの過剰適合を防ぐ手法である。

正則化を導入することで、訓練サンプルに対する過剰な適合が抑えられ、結果として汎化性能の向上が期待できる。

6.3 パラメータ正則化(L1・L2・Elastic Net)

パラメータ正則化では、誤差関数に正則化項を加えることで、重み(パラメータ)の大きさを制御する。一般に、以下の形で誤差関数が拡張される。

$$ E(\boldsymbol{w}) + \lambda \| \boldsymbol{w} \|_p $$

ここで $p$ によって正則化の種類が決まる。

  • L1正則化(p=1)

    重みの絶対値和を抑制し、不要なパラメータを0にしやすい。結果としてスパースなモデルが得られる。

  • L2正則化(p=2)

    重みの二乗和を抑制し、パラメータの発散を防ぐ。重み全体を小さく保つ効果がある。

  • Elastic Net

    L1正則化とL2正則化を組み合わせた手法であり、それぞれの正則化項の強さはハイパーパラメータによって調整される。

6.4 正則化レイヤー ― Dropout

Dropout は学習時にノードをランダムに無効化し、一部の経路を遮断した状態で学習を行う手法である。これにより、特定のノードや経路への依存が抑えられ、より汎化性能の高いモデルが得られる。

Dropout は、複数の異なるネットワークを学習させていると解釈でき、アンサンブル学習と類似した効果を、単一のネットワークで実現していると考えられる。

6.5 正規化レイヤー(Batch / Layer / Instance 正規化)

正規化レイヤーでは、レイヤー間を流れるデータの分布を正規化し、平均0・分散1となるように調整する。これにより、学習の安定化や収束の高速化が期待できる。

  • Batch正規化(Batch Normalization)

    ミニバッチ内の同一チャネルを正規化単位とする。バッチサイズが小さい場合、統計量が不安定になり効果が薄れることがある。

  • Layer正規化(Layer Normalization)

    各サンプルごとに、H×W×C全体を正規化単位とする。ミニバッチサイズに依存しない点が特徴である。

  • Instance正規化(Instance Normalization)

    各サンプルの各チャネルごとに正規化を行う。Batch正規化においてバッチサイズが1の場合と等価な処理と捉えられる。

実装演習

2_9_regularization.ipynb

本実験では、正則化の適用対象を activity_regularizer から kernel_regularizer に変更した。

activity_regularizer は各層の出力(活性値)に対して制約を与えるのに対し、kernel_regularizer重みパラメータそのもの に正則化項を加える。そのため、L1/L2 正則化の本来の目的である「モデルの複雑さ(重みの大きさ)を抑制する」という効果を、より直接的に反映できる。

また、正則化の強さを示す regularization_method_weight0.0001 に設定した。

これは、正則化を弱めにかけることで、学習能力を極端に損なうことなく、過学習のみを緩和することを狙った設定である。

stage_3_2_26.png

  • 正則化なし(None)

学習誤差(loss)はエポックの増加とともに単調に減少している一方で、検証誤差(val_loss)は途中から増加している。

これは、モデルが訓練データに過剰に適合し、汎化性能が低下している 典型的な過学習の挙動 である。

正則化を行わない場合、重みが自由に更新されるため、モデルの複雑さが過剰になりやすいことが確認できる。

  • L1 正則化

L1 正則化を適用した場合、学習誤差・検証誤差ともに減少はするものの、全体として誤差の値は高めで推移している。

これは、正則化係数を 0.0001 としたことで、L1 正則化の影響が比較的強く働き、重みが積極的に 0 に近づけられた結果、モデルの表現力が抑制されすぎている(アンダーフィッティング気味) 状態であると考えられる。

  • L2 正則化

L2 正則化では、学習誤差は着実に減少している一方で、検証誤差は途中から増加している。

正則化なしの場合と比べると過学習の進行はやや緩和されているが、係数 0.0001 では 正則化が十分とは言えず、後半で再び過学習の兆候が現れている。

L2 正則化は重みの発散を抑える効果を持つが、本設定ではその効果が限定的であることが読み取れる。

  • L1 + L2 正則化(Elastic Net)

L1L2 正則化では、学習誤差と検証誤差の差が比較的小さく、全体として安定した推移を示している。

L1 によるスパース化と L2 による重み抑制が同時に働くことで、モデルの複雑さが適度に制御され、過学習と学習不足のバランスが最も良い状態 になっていると考えられる。

2_10_layer-normalization.ipynb

Instance Normalization については、TensorFlow Addons が利用できない環境であったこと、また本課題の主目的が「代表的な正規化手法の挙動比較」にあることから、今回は比較対象から除外した。

stage_3_2_27.png

  • 正規化なし(None)

訓練損失(loss)は単調に減少する一方で、検証損失(val_loss)は途中から増加に転じており、訓練データに過度に適合した 過学習 が確認できた。

  • Batch Normalization

学習初期における val_loss の低下が最も早く、正規化なしの場合と比較して 学習の安定化および汎化性能の改善 が見られた。

ただし、後半では val_loss が再び増加しており、正規化のみでは過学習を完全に防ぐことはできないことが分かる。

  • Layer Normalization

訓練損失は大きく低下したものの、val_loss は早期から増加傾向を示し、汎化性能の改善にはつながらなかった。

CNN 構造では Batch Normalization の方が適している場合が多く、本実験結果もその傾向を支持するものとなった。

2_11_dropout.ipynb

stage_3_2_28.png

Dropout を使用しない場合、訓練損失(loss)は継続的に減少している一方で、検証損失(val_loss)は途中から増加しており、学習が進むにつれて 過学習が発生している ことが確認できる。

一方、Dropout を適用した場合、訓練損失の減少は緩やかになるものの、検証損失は全体として低い水準で推移し、大きな増加は見られなかった。これは、学習時にランダムにノードを無効化することで、特定の特徴や重みに過度に依存することが抑制され、汎化性能が向上した ためと考えられる。

以上より、Dropout は学習を意図的に難しくする代償として、過学習を効果的に抑制し、検証データに対する性能を安定させる手法であることが、本実験結果からも確認できた。

参考図書 / 関連記事

  • ゼロから作る Deep Learning - Pythonで学ぶディープラーニングの理論と実装(斎藤 康毅 著)

この項で扱った正則化および学習安定化手法については、すでに前項までで理論的背景および実装結果を示したとおりであるが、これらはいずれも『ゼロから作る Deep Learning』において体系的に整理されている内容である。

同書では、ニューラルネットワークが高い表現能力を持つ一方で、訓練データへの過適合(過学習)が生じやすいことが指摘されており、その代表的な対策として L1・L2 正則化、Dropout、正規化レイヤーが紹介されている。

特に、重みの大きさを制約する正則化はモデルの自由度を抑制する役割を持ち、Dropout はニューロン間の共適応を防ぐことで汎化性能を向上させる手法として説明されている。

本レポートにおける実装結果でも、正則化や Dropout を導入した場合に、訓練損失と検証損失の乖離が緩和される傾向が確認でき、参考図書で述べられている理論と整合的な挙動が観測された。

また、Batch Normalization などのような正規化手法についても、学習の安定化や収束速度の向上を目的とした技術として位置づけられており、実装演習を通じて、それらの効果と適用条件を実験的に確認することができた。

以上より、この項の内容は参考図書に基づく理論を、実装および結果の考察を通じて具体化したものといえる。

ラビット・チャレンジ - Stage 4. 深層学習 後編 (Day 4)

提出したレポートです。 絶対書きすぎですが、行間を埋めたくなるので仕方ない。 記事が下書きのまま忘れてた。認定取れました。 Rabbit Challenge - Stage 4. 深層学習 後編 (Day 4) 1. 強化学習 強化学習は、 環境と相互作用しながら、長...