ゼロからAI理論を再構築する

ー 文系エンジニアがAIの内部構造をゼロから理解する記録 ー

畳み込みニューラルネットワーク(CNN):画像の構造を活かす

これまでの全結合ネットワークは、画像を1本の長いベクトルに変換してから処理していました。縦横の並びもピクセル同士の隣接関係もすべて捨てて、ただの数値の列として扱う。人間が画像を見るときは隣り合うピクセルの関係からエッジや形を読み取っているのに、全結合層はそれを無視しています。

畳み込みニューラルネットワーク(CNN)は、画像の空間的な構造を保ったまま特徴を抽出するモデルです。

畳み込み層:小さなフィルターをスライドさせる

CNNの基本は畳み込み層です。小さなフィルター(カーネル、たとえば3x3)を画像の上でスライドさせて、フィルターと重なった部分の要素積の和を計算します。

\[ y_{i,j} = \sum_{m} \sum_{n} X_{i+m, j+n} \cdot K_{m,n} \]

全結合層では全ピクセルに個別の重みが必要でしたが、畳み込みでは同じフィルターを画像全体に使い回します。パラメータ共有と呼ばれるこの仕組みで、パラメータ数が大幅に減る。さらに、同じフィルターを使い回すので、画像のどこに特徴があっても検出できます(並進不変性)。

たとえば「縦のエッジを検出するフィルター」が1つあれば、画像の左端でも右端でも同じように縦エッジを拾える。全結合層だと場所ごとに別々の重みでエッジ検出を覚え直す必要があるので、効率が全然違います。

プーリング層:情報をあえて間引く

畳み込みの後にはプーリング層が入ることが多いです。Max Poolingなら、たとえば2x2の領域内の最大値だけを取って残りは捨てます。

情報を捨てて何が嬉しいかというと、物体が数ピクセルずれただけで別物扱いされるのを防げる点です。位置のズレに鈍感になることで、「猫がどこにいても猫と判定する」頑健さが得られます。特徴マップのサイズも小さくなるので計算量が減るという副次的なメリットもあります。

層を重ねると受容野が広がる

CNNを深くすると、後ろの層のニューロンほど入力画像の広い範囲(受容野)を見るようになります。浅い層はエッジや色の変化のような局所的な特徴を捉え、深い層はそれらを組み合わせて目や鼻のようなパーツを認識し、さらに深い層で顔全体や物体を識別する。

前回の多層パーセプトロンの話で「層ごとに抽象度が上がる」と書きましたが、CNNではそれが空間的なスケールの変化としてはっきり見えます。この階層的な特徴抽出が、CNNの画像認識精度が高い理由です。

余談ですが、学習済みのCNNの各層が何を捉えているかを可視化する研究があって、実際に浅い層ではエッジ、深い層ではテクスチャや物体の一部が浮かび上がります。数式だけだと抽象的な話ですが、可視化を見ると「本当に階層的に特徴を捉えている」のが実感できます。

まとめ

CNNは画像の空間構造を壊さずに処理するモデルです。フィルターで局所特徴を抽出し、プーリングで位置ズレへの耐性を持たせ、層を重ねて受容野を広げていく。パラメータ共有のおかげで全結合層よりはるかに効率がいい。

画像以外にも、音声や1次元の系列データにも畳み込みは使われています。次回はCNNとは別のアプローチで系列データを扱うRNNについて書きます。


参考文献

バッチ正規化とドロップアウト:学習を安定させて過学習を防ぐ

前回、ReLUと重みの初期化で勾配消失が解消されたという話を書きました。ただ、勾配が通るようになっただけでは十分ではなくて、深いネットワークには「学習が不安定になる」問題と「訓練データを丸暗記してしまう」問題がまだ残っています。

今回はこの2つに対処するバッチ正規化とドロップアウトについて書きます。

バッチ正規化:各層の入力分布を揃える

深いネットワークを学習させると、パラメータの更新に伴って各層への入力の分布が変わり続けます。前の層の出力が変わるので、次の層から見れば「入力のルールが毎回変わる」ような状態です。内部共変量シフトと呼ばれるこの現象が、学習を不安定にします。

バッチ正規化(Batch Normalization)は、ミニバッチ内のデータの平均を0、分散を1に正規化する処理です。

\[ \hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}} \]

\( \mu_B \) がミニバッチの平均、\( \sigma_B^2 \) が分散。これを活性化関数の前か後に挟みます。入力のスケールが一定に保たれるので、勾配の爆発や消失が起きにくくなり、学習率も大きめに設定できます。結果として学習が速くなる。

実装は nn.BatchNorm1dnn.BatchNorm2d を層の間に入れるだけです。効果が大きいので、現在ではほぼ標準的に使われています。

ドロップアウト:ニューロンをランダムに無効化する

過学習の問題は前々回にも触れましたが、パラメータが多い深いネットワークでは特に起きやすいです。訓練データのノイズまで覚えてしまい、未知のデータで性能が出ない。

ドロップアウト(Dropout)は、学習の各ステップでランダムに一部のニューロンを無効化して、残りだけで学習を進める手法です。特定のニューロン同士が強く結びついて決まったパターンに依存するのを防げます。毎回違う組み合わせで学習するので、個々のニューロンがより汎用的な特徴を学ぶようになる。理論的には、毎回異なるサブネットワークを学習してアンサンブルしているのに近い効果があるとされています。

ニューロンを消して性能が上がるというのは直感に反する気もしますが、実際にドロップアウトを入れるだけで検証データの精度が改善するケースは多いです。

学習時と推論時で挙動が違う

バッチ正規化もドロップアウトも、学習時と推論時で動作が変わります。

バッチ正規化は学習時にミニバッチの統計量を使い、推論時は学習中に計算した移動平均を使います。ドロップアウトは学習時にニューロンを消し、推論時は全ニューロンを使って出力をスケーリングします。

PyTorchだと model.train()model.eval() の切り替えがこれに対応していて、切り替え忘れると推論時にドロップアウトがかかったままになったりします。学習はうまくいっているのに推論の結果がおかしいときは、だいたいここが原因です。自分も何度かやりました。

まとめ

バッチ正規化で各層の入力分布を安定させ、ドロップアウトで特定パターンへの依存を防ぐ。どちらも実装が簡単で効果が大きく、深層学習ではほぼ必ず使われます。

ここまでで、全結合型のニューラルネットワークに必要な道具は一通り揃いました。次回は、画像のような空間的な構造を持つデータに特化した畳み込みニューラルネットワーク(CNN)について書きます。


参考文献

勾配消失とReLU:深いネットワークが学習できなかった理由

前回、層を重ねると抽象度の異なる特徴を段階的に抽出できるという話を書きました。ただ、2010年代初頭まで、ネットワークを深くすると学習がうまくいかないという問題がありました。理論的には深いほうがいいのに、実際に深くすると性能が出ない。

今回はその原因である勾配消失問題と、解決策として普及したReLU、そして重みの初期化について書きます。

シグモイドの微分が小さすぎる

深層ネットワークの学習では、出力側で計算した誤差を連鎖律で入力側に伝播させます。ここでシグモイド関数を活性化関数に使っていると問題が起きます。

シグモイド関数の微分値は最大でも0.25です。層を1つ遡るたびに勾配にこの0.25以下の値が掛かるので、10層も遡れば勾配はほぼ0になります。入力に近い層の重みがほとんど更新されなくなる。

これが勾配消失問題(Vanishing Gradient Problem)です。層を深くするメリットが、活性化関数の微分の小ささで打ち消されていました。逆に、初期値が大きすぎると勾配が層を遡るごとに膨らんで発散する「勾配爆発」も起きます。どちらにしても学習が進まない。

ReLUが問題を解いた

ReLU(Rectified Linear Unit)は見た目も中身も単純な関数です。

\[ f(z) = \max(0, z) \]

入力が正ならそのまま通し、負なら0にする。微分は正の領域で常に1です。

シグモイドだと層ごとに0.25をかけていたのが、ReLUなら1をかけるだけなので、何層遡っても勾配が小さくならない。これで勾配消失が解消されます。

シグモイドのような滑らかな曲線のほうが脳に近いと長く考えられていたので、こんな折れ線グラフみたいな関数がうまくいくのは当時意外だったらしく、2012年のAlexNetが画像認識コンペで圧勝したあたりから急速に広まりました。自分も最初に知ったとき、こんなに単純でいいのかと拍子抜けした覚えがあります。

重みの初期化も大事

活性化関数を変えても、重みの初期値が悪いと学習は進みません。

全ての重みを0で始めると、すべてのニューロンが同じ出力を返して対称性が崩れず、学習が止まります。大きすぎると勾配爆発、小さすぎると勾配消失。

現在は層のノード数に合わせて初期値の分散を調整する方法が標準です。シグモイド系にはXavier初期化、ReLU系にはHe初期化が使われています。PyTorchではデフォルトで設定されているので自分で触らないことも多いですが、学習がうまくいかないときに初期化を見直すのは基本的なデバッグ手順の一つです。

まとめ

深いネットワークが学習できなかったのは、シグモイドの微分が小さすぎて勾配が入力側に届かなかったからです。ReLUと適切な初期化でこの問題が解消され、深層学習が実用になりました。

ただ、層を深くしてパラメータを増やせば過学習のリスクも上がりますし、学習の安定性にも別の工夫が要ります。次回はバッチ正規化とドロップアウトについて書きます。


参考文献

多層パーセプトロン:層を重ねると何が変わるか

前回、線形計算と活性化関数の組み合わせでモデルが分類をできるようになるという話を書きました。ただ、あれは1層だけの話です。現実の問題、たとえば入り組んだ境界線を持つデータの分類や画像認識は、1層では表現力が足りません。

今回は、この層を複数重ねた多層パーセプトロン(MLP)がなぜ強力なのかを整理します。

構造はシンプルな繰り返し

多層パーセプトロンの構造は、前回の「線形計算+活性化関数」を縦と横に並べたものです。

\[ h = \sigma(W_1 x + b_1) \] \[ \hat{y} = \sigma(W_2 h + b_2) \]

\( h \) は隠れ層(Hidden Layer)の出力です。最初の層が入力 \( x \) を別の空間 \( h \) に変換し、次の層がその \( h \) から予測を出します。

たとえばコーヒーの味を予測するなら、第1層が「豆の香りと色から焙煎度合いを判断する」、第2層が「焙煎度合いと温度から味を予測する」というように、段階的に情報を加工しているイメージです。

1層でも理論上は十分、でも深いほうが効率的

万能近似定理(Universal Approximation Theorem)という定理があって、十分な数のニューロンを持つ隠れ層が1層あれば、どんな連続関数でも近似できるとされています。

ただ、1層で済ませようとすると必要なニューロン数が爆発的に増えます。層を深くしたほうが少ないパラメータで同じ関数を表現できることが多い。

深層化の効果を画像認識で考えると、第1層がエッジや色の変化を捉え、第2層がそれらを組み合わせて形を捉え、第3層が形の組み合わせから物体を認識する、という具合に抽象度が段階的に上がっていきます。各層が前の層の出力をさらに抽象化するので、浅いネットワークでは大量のニューロンが必要な表現を、深いネットワークでは少ない計算で実現できます。

パラメータが増えすぎる問題

層を増やしてニューロンを増やすほど、最適化すべき重み \( W \) の数は膨大になります。

表現力が上がること自体はいいのですが、上がりすぎるとモデルが訓練データのノイズまで丸暗記してしまう。過学習(Overfitting)です。訓練データでの損失は下がるのに、未知のデータでの性能が伸びない状態で、学習曲線を見ると訓練と検証の損失が途中から乖離し始めるのでわかります。

過学習の対策(正則化やドロップアウトなど)については後の記事で扱いますが、モデルを複雑にするほど「訓練データに適応しすぎる」リスクが上がるという点は、ここで押さえておく必要があります。

まとめ

多層パーセプトロンは、線形計算と活性化関数を積み重ねて複雑なパターンを表現するモデルです。層が深いほど少ないパラメータで高い表現力が得られますが、パラメータの総数は増え、過学習も起きやすくなります。

層を深くするとさらに別の問題も出てきます。勾配が層を遡るうちに消えたり爆発したりする現象です。次回はこの勾配消失・勾配爆発の問題と、ReLU関数や重みの初期化による対処について書きます。


参考文献

活性化関数とロジスティック回帰:直線を曲げて分類する

前回、線形モデルは入力と出力の関係を直線でしか表せないという話を書きました。ただ、現実の問題には「合格か不合格か」「スパムか否か」のように、はい/いいえで答える分類タスクが多くあります。線形モデルの出力は実数なので、このままでは分類には使いにくい。

今回は、線形モデルの出力を確率に変換する仕組みと、それに伴って損失関数も変わるという話です。

シグモイド関数で出力を確率にする

線形モデルの出力 \( f(x) = w^T x + b \) は、どんな大きな値も小さな負の値も取り得ます。これを0から1の範囲に収めて「確率」として扱えるようにするのがシグモイド関数です。

\[ \sigma(z) = \frac{1}{1 + e^{-z}} \]

線形モデルの出力をこの関数に通すと、

\[ \hat{y} = \sigma(w^T x + b) \]

出力が常に0から1の間に収まります。これがロジスティック回帰です。名前に「回帰」と付いているのが紛らわしいのですが、実態は分類モデルです。

出力が0.8なら「80%の確率でクラス1」と解釈できるので、閾値(たとえば0.5)を決めてそれより上なら1、下なら0と判定します。

活性化関数とは何か

シグモイド関数のような、入力を非線形に変換する関数を総称して活性化関数(Activation Function)と呼びます。名前の由来は脳のニューロンで、一定以上の刺激で発火する仕組みに着想を得ています。

なぜ非線形な変換が必要かというと、線形な計算をどれだけ重ねても結果は線形のままだからです。\( f(g(x)) \) で \( f \) も \( g \) も線形なら、合成しても線形。層を何百重ねても直線一本と変わりません。

層の間に非線形な変換を挟むことで、モデルは曲線や曲面を表現できるようになります。ディープラーニングが複雑なパターンを捉えられるのは、この非線形性のおかげです。ここは理屈としてはわかるのですが、「なぜシグモイドなのか」「他の関数ではだめなのか」が気になるところで、実際ReLUなど別の活性化関数もよく使われます。この辺りは後の記事で触れる予定です。

損失関数も変わる:交差エントロピー

モデルの出力が確率になると、損失関数も二乗誤差から交差エントロピー誤差に変わります。

\[ L(\hat{y}, y) = - [y \log \hat{y} + (1 - y) \log (1 - \hat{y})] \]

正解が1なのに予測が0に近いと、 \( \log \) の性質で損失が急激に大きくなります。正解を高い確率で当てていれば損失はほぼ0。間違いの度合いに応じてペナルティが非線形に増えるので、確信を持って外したときに特に厳しく効きます。

二乗誤差でも分類はできなくはないのですが、交差エントロピーのほうが勾配の振る舞いが良く学習が安定しやすいので、確率を出力するモデルではほぼこちらが使われています。

まとめ

線形モデルの出力をシグモイド関数に通して確率にし、交差エントロピーで損失を測る。これでモデルは分類ができるようになります。

「線形計算→活性化関数」の組み合わせは、そのままニューラルネットワークの1層分の構造です。次回は、この層を積み重ねたときに何が起きるのか、多層パーセプトロンについて書きます。


参考文献

線形モデル:直線で世界を近似する

ディープラーニングがどれだけ複雑に見えても、一番小さい単位に分解すると線形な計算が出てきます。入力に重みをかけて足す。今回はこの線形モデルについて、何ができて何ができないのかを整理します。

出力は入力の重み付き和

線形モデルの考え方はシンプルで、「出力は各入力に重要度をかけて合計したもの」という仮定です。

\[ f(x) = w^T x + b \]

\( w \) が重み、\( b \) がバイアス(切片)です。

たとえば「コーヒーの美味しさ」を予測するなら、「豆の質」に3、「お湯の温度」に1、「淹れる時間」に-0.5をかけて足し合わせるようなイメージです。各パラメータの意味がそのまま読めるので、「なぜその予測になったのか」を人間が追いやすい。線形モデルが説明性の高いモデルだと言われるのはこの点です。

二乗誤差で最適化する

線形モデルでよく使われる損失関数は二乗誤差です。

\[ L(f(x), y) = (f(x) - y)^2 \]

差を二乗するので、大きく外れた予測ほどペナルティが重くなります。裏を返せば、極端な外れ値に引っ張られやすいのですが、計算が扱いやすいこともあって広く使われています。

線形モデルと二乗誤差の組み合わせ(最小二乗法)には、勾配降下法で少しずつ下らなくても行列演算で一発で最適解が出るという性質があります(正規方程式)。モデルが線形だから成り立つ話で、非線形なモデルではこうはいきません。自分はこの事実を知ったとき、前回までの勾配降下法の話は何だったのかと一瞬思いましたが、線形モデル以外では正規方程式が使えないので、やはり勾配降下法は必要です。

直線では表現しきれないもの

線形モデルは入力と出力の関係が比例的であることを前提にしています。当然、そうでない場合のほうが多い。

ある温度を超えると急に味が変わる、複数の要素が組み合わさって初めて効果が出る、画像のピクセルの並びから「猫」を判定する。どれも直線一本で表せる話ではありません。こういうケースに線形モデルを当てると、データの構造を捉えきれずに予測精度が頭打ちになります。未学習(Underfitting)と呼ばれる状態です。

まとめ

線形モデルは、複雑な現象を「要素の足し算」として捉えてみるところから始まります。解釈しやすく、最適化も楽ですが、表現力は限られています。

この限界があるから、直線を曲げる方向に進むことになります。次回は活性化関数とロジスティック回帰について書きます。


参考文献

ミニバッチ学習とSGD:全部見なくても学習できる理由

前回、勾配降下法で損失を減らしていくという話を書きました。ただ、あの説明は「全データの勾配をまとめて計算する」前提になっていて、データが数百万件ある現実のタスクだとそのまま使うのは厳しいです。1回パラメータを更新するだけで全データを走査するので、とにかく遅い。

今回は、この計算コストの問題をどう回避するかという話です。

バッチ勾配降下法の問題

理論通りの勾配降下法(バッチ勾配降下法)では、全データ \( n \) 個に対する損失の平均勾配を計算します。

\[ \nabla \hat{R}(w) = \frac{1}{n} \sum_{i=1}^{n} \nabla L(f(x_i), y_i) \]

全データを使うので勾配の推定は正確です。ただ、 \( n \) が大きいと1ステップの計算に時間がかかりすぎて、実用的ではありません。

SGDは1点だけ見て更新する

確率的勾配降下法(SGD)は割り切りがすごくて、全データの中から1つだけランダムに選んで、その1点の勾配でパラメータを更新します。

\[ w_{t+1} = w_t - \eta \nabla L(f(x_i), y_i) \]

1点しか見ていないので、更新の方向はかなりブレます。全体の谷底とは違う方向に進んでしまうことも普通にある。ただ、繰り返せば平均的には正しい方向に向かうことが理論的に保証されています。

このブレ(ノイズ)には副産物もあって、損失の地形に浅い窪み(局所解)がある場合、正確な勾配だとそこにハマって出られなくなることがあります。SGDのノイジーな更新は、浅い窪みを飛び越える効果があるとされています。欠点に見えるものが利点にもなるというのは、自分がこの分野を勉強していて面白いと感じるところです。

ミニバッチが実質的な標準

全データだと遅い、1点だと不安定。その間を取ったのがミニバッチ学習です。

データを \( m \) 個(32や64、256あたりが多い)のグループに分けて、グループ内の平均勾配で更新します。

\[ \nabla \hat{R}_{batch}(w) = \frac{1}{m} \sum_{j=1}^{m} \nabla L(f(x_j), y_j) \]

SGDより勾配の推定が安定するのに加えて、GPUとの相性がいいのも大きいです。GPUは同じ計算を大量に並列で回すのが得意なので、ミニバッチ単位で処理するとかなり速くなります。PyTorchやTensorFlowも基本的にミニバッチ前提で設計されていて、データローダーにバッチサイズを指定するだけで勝手にこの仕組みが動きます。

まとめ

全データを使わず、断片的なサンプルで勾配を推定して更新を繰り返す。精度は落ちるけど速いし、ノイズが局所解の回避に役立つこともある。ミニバッチはそのちょうどいい落としどころです。

次回は、ここまで暗黙の前提にしてきた「線形モデル」について、何ができて何ができないのかを整理します。


参考文献