本節の重点は、トレーニングプロセスにおける神経ネットワークの活性化、特に下流の勾配について深く印象を持ち、理解することです。これらの構造の発展の歴史を理解することは重要です。なぜなら、RNN(リカレントニューラルネットワーク)は、一般的な近似器(universal approximator)として、原則的にすべてのアルゴリズムを実現できるからですが、いくつかの勾配主導の技術で最適化するのは容易ではありません。その最適化が難しい理由は理解の重点であり、トレーニング中の活性化と勾配の挙動を観察することで結論を導き出すことができます。この状況を改善しようとする多くの変種も見られます。
起点:MLP#
前章の MLP モデルを再確認し、現在の起点としましょう:
前章のコードを基に修正し、ハードコーディングを行わず、後の調整を容易にします。
# MLP revisited
n_embd = 10 # 文字埋め込みベクトルの次元
n_hidden = 200 # 隠れ層の隠れユニットの数
# vocab_size = 27, block_size = 3
g = torch.Generator().manual_seed(2147483647) # 再現性のため
C = torch.randn((vocab_size,n_embd), generator=g)
W1 = torch.randn((n_embd * block_size, n_hidden), generator=g)
b1 = torch.randn(n_hidden, generator=g)
W2 = torch.randn((n_hidden, vocab_size), generator=g)
b2 = torch.randn(vocab_size, generator=g)
paramters = [C, W1, b1, W2, b2]
print(sum(p.nelement() for p in paramters)) # 総パラメータ数
for p in paramters:
p.requires_grad_()
総パラメータ数は前章と同じ 11897 です。
神経ネットワークのトレーニング部分も同様に機能を変更しない修正を行います:
# 前章と同様のミニバッチ最適化
max_steps = 200000
batch_size = 32
lossi = []
for i in range(max_steps):
# ミニバッチ
ix = torch.randint(0,Xtr.shape[0],(batch_size,), generator=g)
Xb, Yb = Xtr[ix], Ytr[ix] # バッチ X, Y
# フォワードパス
emb = C[Xb] # 埋め込み
embcat = emb.view(emb.shape[0], -1) # すべての埋め込みベクトルを結合
hpreact = embcat @ W1 + b1 # 隠れ層の前処理
h = torch.tanh(hpreact) # 隠れ層
logits = h @ W2 + b2 # 出力層
loss = F.cross_entropy(logits, Yb) # 損失関数
# バックワードパス
for p in parameters:
p.grad = None
loss.backward()
# 更新
lr = 0.1 if i < 100000 else 0.01
for p in parameters:
p.data += -lr * p.grad
# 統計を追跡
if i % 10000 == 0:
print(f'{i:7d}/{max_steps:7d}: {loss.item():.4f}')
lossi.append(loss.log10().item())
plt.plot(lossi)
注意:ここで最初の損失関数は 27 に達し、その後すぐに非常に低くなりました。原因を推測してみてください。
曲棍子のように (a hockey stick)
損失関数の可視化も、インデックスで異なる部分を分割した損失機能を作成しました:
@torch.no_grad() # このデコレーターは、この関数内のすべての勾配の追跡を無効にします
def split_loss(split):
x,y = {
'train': (Xtr, Ytr),
'val': (Xdev, Ydev),
'test': (Xte, Yte),
}[split]
# フォワードパス
emb = C[x] # (N, block_size, n_embd)
embcat = emb.view(emb.shape[0], -1) # (N, block_size * n_embd)に結合
h = torch.tanh(embcat @ W1 + b1) # (N, n_hidden)
logits = h @ W2 + b2 # (N, vocab_size)
loss = F.cross_entropy(logits, y)
print(split, loss.item())
split_loss('train')
split_loss('val')
このデコレーターの効果は、各テンソルに
requires_grad = False
を追加するのと同じで、backward()
を呼び出さないため、効率に影響を与えるグラフを下層で維持する必要がありません。
ここでの損失は:
train 2.2318217754364014
val 2.251192569732666
# モデルからサンプルを取得
g = torch.Generator().manual_seed(2147483647 + 10)
for _ in range(20):
out = []
context = [0] * block_size # すべてを初期化 ...
while True:
# フォワードパス
emb = C[torch.tensor([context])] # (1, block_size, n_embd)
h = torch.tanh(emb.view(1, -1) @ W1 + b1)
logits = h @ W2 + b2
probs = F.softmax(logits, dim=1)
# 分布からサンプリング
ix = torch.multinomial(probs, num_samples=1, generator=g).item()
# コンテキストウィンドウを移動し、サンプルを追跡
context = context[1:] + [ix]
out.append(ix)
# 特殊な'.'終端記号をサンプリングした場合はbreak
if ix == 0:
break
print(''.join(itos[i] for i in out))
サンプリング結果:
carlah. amorilli. kemri. rehty. sacessaeja. huteferniya. jareei. nellara. chaiir. kaleig. dham. jore. quint. sroel. alian. quinaelon. jarynix. kaeliigsat. edde. oia.
パフォーマンスはまだあまり良くありませんが、Bigram よりは強いです。
初期化修正#
まず、上記のように初期損失が大きすぎるため、神経ネットワークの初期化ステップに問題があります。
初期化時に何を望んでいるのか?
予測する次の文字は 27 種類の可能性があり、どの文字も他の文字よりも可能性が高いとは言えません。したがって、最初の確率分布は均等に分配されることを期待しています(1/27)。ここで正しい損失がどれくらいか手動で計算してみましょう:
27 よりもはるかに小さいです。
これは、最初に神経ネットワークがランダムに割り当てられたため、文字間の確率分布の差が大きくなったためです。ブレークポイントを打って確認できます:
logits はすべておおよそ 0 であるべきですが、現在のように非常に極端な分布は、誤った自信をもたらし、損失を大きくします。
logits はh @ W2 + b2
として定義されているため、これらのパラメータを小さくします:
W2 = torch.randn((n_hidden, vocab_size), generator=g) * 0.01
b2 = torch.randn(vocab_size, generator=g) * 0
logits はすべて 0 に近くなりました。
我々が望む損失に非常に近いです(上記の 3.2985 に言及)。
では、W2
を 0 に設定できるでしょうか?そうすれば最低の損失を得られるのではないでしょうか?
神経ネットワークの重みパラメータを正確に 0 に設定することは望ましくありません。これは対称性の破壊問題を引き起こし、ネットワークが有用な特徴を学習するのを妨げるため、非常に小さな数字に設定する必要があります。
現在の損失にはまだいくつかのエントロピーがあり、これはsymmetry breakingと呼ばれます。
devv.ai
で名詞の説明を見てみましょう。このサイトをお勧めします。非常に便利です。
ブレークを取り除き、最適化を検証した結果、期待される結果が得られました:
0/ 200000: 3.3221
10000/ 200000: 2.1900
...
190000/ 200000: 1.9368
train 2.123051404953003
val 2.163424015045166
現在、損失関数の初期値は非常に正常で、グラフはもはやhocky stickのようではなく、最終的なパフォーマンスも向上しました。パフォーマンス向上の理由は、神経ネットワークの最適化により多くのサイクルを使用したためであり、最初の数千回の反復を過度に高い初期重みの圧縮に費やすことはありませんでした。
勾配消失#
h と hpreact を可視化します:
隠れ層の多くの活性化値が ±1 に分布していることがわかります。これは、pre-action 部分が過大な範囲に分布しているためです(絶対値が大きい入力に対して、tanh の出力は ±1 に飽和します)。
これは実際には非常に良くありません。micrograd で実装した tanh とその逆伝播を思い出してください。tanh は常に一定の比率で勾配を減少させます。tanh ユニットを通過すると、勾配が消失します。なぜなら、tanh は出力が ±1 に近づくと、平坦な尾部にあり、損失に対する影響が非常に小さく、勾配は 0 になるからです。そして、もし t が 0 であれば、神経元は活性化しません(勾配はそのまま出力されます)。
def tanh(self):
# ... フォワードパスコード
def _backward():
self.grad += (1 - t**2) * out.grad
out._backward = _backward
return out
もし神経元のすべての例が ±1 であれば、その神経元は完全に学習していないと言えます。これは「死んだ神経元」と呼ばれます。
すべての神経元が完全に白色(絶対値 > 0.99=True)であることは観察されていませんが、神経元はまだ学習します。しかし、初期化時に ±1 の事例がこれほど多くないことを望んでいます。
これは他の神経ネットワークで使用される非線形活性化関数にも当てはまります。たとえば、Sigmoidも同様にsquashing neuron(圧縮作用の)関数です。ReLUの場合は、pre-actionが負のときに勾配消失が発生し、逆伝播時に勾配がゼロになります。初期化時に発生する可能性があるだけでなく、学習率が高すぎる場合、逆伝播中に特定の神経元が非常に大きな勾配更新を受け取る可能性があります。この大きな勾配更新は、神経元の重みが極端な値に更新され、その後のフォワードパス中にすべての入力データに対して活性化を生成しなくなることを引き起こす可能性があります(つまり、出力が常に 0 になります)。この現象は、神経元が「ノックアウト」される(knocked out)と形容され、死んだ神経元となり、永久的な脳損傷のようになります。一般的に使用されるELUにもこの問題があります。
Leaky ReLUはこの問題を持たず、常に勾配を得ることができます。
重みの初期化#
問題の根源であるhpreact
は、埋め込み後に w1+b1 を掛けたもので、0 から遠すぎます。そして、我々が望むものは、以前の logits に対する期待と非常に似ています:
W1 = torch.randn((n_embd * block_size, n_hidden), generator=g) * 0.1
b1 = torch.randn(n_hidden, generator=g) * 0.01
現在のヒストグラムはずっと良くなりました。これは、pre-action 時に分布範囲が (-2,1.5) に減少したためです。
現在、0.99 以上の神経元はもはや存在しません。
W1 の係数を 0.2 に増やすと、満足のいく結果が得られました:
-
元の:
train 2.2318217754364014
val 2.251192569732666 -
softmax の過度な自信の問題を修正:
train 2.123051404953003
val 2.163424015045166 -
tanh 層の過度な飽和問題を修正:
train 2.035430431365967
val 2.1064531803131104
見ての通り、初期化が非常に悪くても、このネットワークはある程度の特徴を学習しました。しかし、これは単層 MLP ネットワークが非常に浅いため、最適化の問題が非常に簡単であり、これらの誤りに対して比較的寛容であるからです。より深いネットワークに直面した場合、これらの問題は深刻な影響を及ぼします。
では、大きくて深い神経ネットワークに対して、これらの調整スケールをどのように設定すればよいのでしょうか?
初期化戦略#
事前に活性化時に、2 つのガウス分布を掛け合わせたときの平均と標準偏差がどのように変化するかを観察してみましょう。
平均は依然として 0 に近いですが、これは対称的な操作によるものですが、標準偏差は 3 倍になったため、このガウス分布は拡張されています。
神経ネットワークでは、初期化時に各層の活性化分布に大きな差がないことを望み、勾配消失または勾配爆発の問題を回避します。各層の活性化値の分布が広すぎたり狭すぎたりすると、ネットワークのトレーニングが不安定になる可能性があります。理想的には、ネットワークは初期段階で良好な活性化分布を持ち、情報と勾配がネットワーク内で効果的に伝播できるようにする必要があります。
ここで関連する概念は、内部共変量シフト(Internal Covariate Shift)です。
では、スケールの比率をどのように設定すればよいのでしょうか?
w = torch.randn(10, 200) * 0.3
'''
tensor(-0.0237) tensor(1.0103)
tensor(-0.0005) tensor(0.9183)
すでに近づいていますが、ちょうど標準正規分布になるにはどうすればよいでしょうか?
'''
神経ネットワークの重み初期化プロセスでは、通常、重みの初期値を標準化し、各神経元の重みをその入力接続数(fan-in)の平方根で割ります。
x = torch.randn(1000, 10)
w = torch.randn(10, 200) / 10**0.5 # 入力要素数は10
この問題に関してよく引用されるのは、Delving Deep into Rectifiers: Surpassing Human-Level Performance on Image Net Classificationです。この論文は、畳み込み神経ネットワークにおいて、特に ReLU と P-ReLU の非線形性(nonlinearity)を研究しました。前述のように、ReLU は負の活性化をすべてゼロにし、分布の半分を捨てるため、神経ネットワークのフォワードパスでこれを補償する必要があります。研究では、重みの初期化には標準偏差がのzero-mean Gaussianを使用する必要があることが示されましたが、ここで使用しているのはです。これは、ReLU が分布の半分を捨てるためです。
この点は PyTorch に統合されています。リンクtorch.nn.init:
torch.nn.init.kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu', generator=None)
この関数のパラメータの意味は次のとおりです:
tensor
: 初期化するテンソル。a
: 正規分布の平均、デフォルトは 0。mode
: 標準偏差を計算するモード。選択肢は 'fan_in' と 'fan_out' で、デフォルトは 'fan_in' です。'fan_in' は入力用、'fan_out' は出力用です。nonlinearity
: 非線形活性化関数のタイプ。選択肢はtorch.nn.functional
の活性化関数で、デフォルトはtorch.nn.functional.relu
です。
論文の結論によれば、異なるnonlinearity活性化関数に対して、対応するゲインも異なります:
ここでの ReLU のゲインは、上記のに対応しています。
使用している tanh もこのようなゲインが必要です。理由は、ReLU が負を 0 に圧縮するのと同様に、tanh は尾部を圧縮するため、この圧縮操作のゲインを防ぎ、分布を標準正規分布に戻す必要があるからです。
数年前の研究では、神経ネットワークは初期化問題に非常に脆弱でした。しかし、現代の技術革新、たとえば残差ネットワーク、さまざまな正規化層、Adam などのより良い最適化器により、「初期化を完全に正しく保証する」ことは相対的にそれほど重要ではなくなりました。
実践では、以前のゲインを変更し、次の式に従って調整します:
W1 = torch.randn((n_embd * block_size, n_hidden), generator=g) * (5/3)/((n_embd * block_size)**0.5) # 0.2
再トレーニングを行い、近似的な結果を得ました。
バッチ正規化#
バッチ正規化
影響力のあるBatch Normalizationは、2015 年に Google のチームによって提案され、深層神経ネットワークのトレーニングを可能にしました。
論文が提案する核心的な考え方は、隠れ層の状態(私たちのhpreact
に対応)を標準正規分布に直接修正できるということです。
hpreact = embcat @ W1 + b1 # 隠れ層の前処理
hpreact = (hpreact - hpreact.mean(0, keepdim=True)) / hpreact.std(0, keepdim=True) # バッチ正規化
これにより、深層ネットワークにおいても、各神経元の発火率(すなわち活性化値)が初期化段階で有利な勾配降下最適化に適した近似ガウス分布を維持できることが保証されます。しかし、その後、逆伝播によって、この分布がどのように変化すべきか、より鋭くするべきか、より分散させるべきかを教える必要があります。これにより、一部の神経元がより容易にtrigger happy(活性化)またはより難しくなることがあります。
したがって、スケールとシフトのステップも必要であり、正規化された分布にゲインを加え、バイアスを加えてこの層の出力を得ます。
# パラメータを追加
bngain = torch.ones((1, n_hidden))
bnbias = torch.zeros((1, n_hidden))
parameters = [C, W1, b1, W2, b2, bngain, bnbias]
# スケールとシフト
hpreact = bngain * (hpreact - hpreact.mean(0, keepdim=True)) / hpreact.std(0, keepdim=True) + bnbias # バッチ正規化
初期化時、ゲインとバイアスはそれぞれ 1 と 0 であり、この時点で分布は私たちが望む標準正規分布です。これらはすべて微分可能であり、後の最適化プロセスで逆伝播を行うことができます。
バッチ正規化層を追加した後のパフォーマンスは:
train 2.0668270587921143
val 2.104844808578491
以前と比較して大きな変化はありません。これは、私たちが直面しているのが単一の隠れ層を持つ単純な例であり、hpreact を大まかにガウス分布に変えるための重み行列(W1)のスケールを直接計算できるためです。バッチ正規化はここではあまり効果を発揮しません。しかし、より深い神経システムにおいて、多くの異なるタイプの操作がある場合、重み行列のスケールを調整するのは非常に困難になります。このため、神経ネットワークのさまざまな層に均等にバッチ正規化層を追加する方がはるかに簡単です。一般的な習慣は、線形層(linear layer)と畳み込み層(convolution layer)の後に、バッチ正規化層を追加して、神経ネットワークの各ポイントでこれらの活性化のスケールを制御することです。
バッチ正規化は実践で非常に効果的であり、活性化と分布を効果的に制御する正則化(Regularization)の副作用があります。これは、バッチの選択がランダムであるため、追加の「エントロピー(entropy)」を導入し、モデルの過剰適合のリスクを低下させることができます。
Andrej Karpathy は、バッチ正規化が数学的に「結合されている」と述べています。つまり、この技術はネットワーク層間の統計分布を相互依存させます。トレーニングの効率のために、私たちは複数のバッチに分けましたが、1 つのバッチのデータ内での各データポイントの正規化は、全バッチの平均と分散に依存しているため、以下のいくつかの問題を引き起こします:
-
バッチ依存性:バッチ正規化は小バッチデータの統計特性に依存しているため、モデルのパフォーマンスはバッチサイズの影響を受ける可能性があります。小さなバッチの場合、平均と分散の推定が大きな分散を持つ可能性があり、トレーニングが不安定になります。
-
ドメインの変化:異なるドメイン(トレーニングと推論など)では、データ分布が大きく異なる可能性があり、バッチ正規化はこれらの異なるドメインで一貫した動作を維持する必要があります。この問題を解決するために、通常はトレーニング後に移動平均を使用して、全トレーニングセットの平均と分散を推定し、推論に使用します。
-
結合された勾配:バッチ正規化層は逆伝播時に勾配を計算する際に全バッチのデータポイントを考慮する必要があるため、単一のデータポイントの勾配更新はもはや独立ではありません。この結合は、モデルの最適化プロセスにおける勾配の方向と大きさを制限する可能性があります。
これらの理由から、研究者たちは、線形正規化(Linear Normalization)、層正規化(Layer Normalization)、グループ正規化(Group Normalization)、インスタンス正規化(Instance Normalization)などの代替正規化技術を探し続けています。
バッチ正規化の推論問題#
トレーニング段階では、各バッチの mean と std はリアルタイムで計算され、現在のバッチデータを正規化するために使用されます。しかし、モデルをデプロイして推論を行う際には、通常は1 つのサンプルを処理するか、トレーニング時とは異なるバッチサイズで処理するため、単一サンプルの統計データを使用してバッチ正規化を行うことはできません。これにより、過度の分散と不安定な予測が生じます。
この問題を解決するために、論文で提案された方法は、バッチ正規化がトレーニング中に全データセットの平均と分散の移動平均を計算することです(moving average)。これらの移動平均は、その後、推論時に単一バッチの統計データの代わりに使用されます。モデルが生産環境にデプロイされると、これらの移動平均がリアルタイムで計算されたバッチの平均と分散の代わりに使用されます。これにより、推論時のバッチサイズに関係なく、モデルはトレーニングデータ上で計算された安定した統計データを使用します。
# トレーニング終了後にバッチ正規化をキャリブレーション
with torch.no_grad():
# トレーニングセットを通過させる
emb = C[Xtr]
embcat = emb.view(emb.shape[0], -1)
hpreact = embcat @ W1 + b1
# 平均と標準偏差を計算
bnmean = hpreact.mean(0, keepdim=True)
bnstd = hpreact.std(0, keepdim=True)
トレーニングセットと検証セットでパフォーマンスを評価する際、動的に更新された std と mean を、得られた全体の平均 std、mean に置き換えます:
# hpreact = bngain * (hpreact - hpreact.mean(0, keepdim=True)) / hpreact.std(0, keepdim=True) + bnbias
hpreact = bngain * (hpreact - bnmean) / bnstd + bnbias
これで、単一のサンプルを推論に渡すこともできます。
しかし、実際には、誰も平均と標準偏差の推定を第二段階(すなわち神経ネットワークのトレーニング後の推論段階)に置きたくはありません。論文では、この問題に対するアイデアが提案されています:トレーニング中に神経ネットワークの過程でこれらの値を推定することができます。
まず、これらの値の実行時形式を定義し、初期化します:
# バッチノーマル化のパラメータ
bngain = torch.ones((1, n_hidden))
bnbias = torch.zeros((1, n_hidden))
bnmean_running = torch.zeros((1, n_hidden))
bnstd_running = torch.ones((1, n_hidden))
以前に述べたように、各 preact の分布が標準ガウスに近づくように W1 と b1 を初期化しました。したがって、平均は約 0、標準偏差は約 1 です。
# バッチノーマル化層
# -------------------------------------------------------------
bnmeani = hpreact.mean(0, keepdim=True)
bnstdi = hpreact.std(0, keepdim=True)
hpreact = bngain * (hpreact - bnmeani) / bnstdi + bnbias
次に、トレーニング中にそれらを更新します。PyTorch では、これらの平均と標準偏差は勾配降下最適化方式に基づいておらず、決してそれらの勾配を導出することはありません。
with torch.no_grad():
bnmean_running = 0.999 * bnmean_running + 0.001 * bnmeani
bnstd_running = 0.999 * bnstd_running + 0.001 * bnstdi
# -------------------------------------------------------------
ここでの 0.999 と 0.001 は、移動平均の減衰係数(decay factor)の例です。減衰係数は、移動平均における過去の値と新しい値の相対的重要性を定義します。より具体的には:
- 0.999(
momentum
)は移動平均の減衰因子であり、以前に蓄積された実行平均と分散がどれだけ保持されるかを決定します。 - 0.001(1 - momentum に等しい)は新しい値の重みを示し、これは各バッチの平均と分散が更新時に持つ重みです。
論文では、次のステップもあります:
たとえば、はデフォルトでであり、基本的にゼロで割るのを防ぐ役割を果たします。これはBATCHNORM1D
の eps パラメータに対応します。
さらに、不要な部分があります:
# 線形層
hpreact = embcat @ W1 # + b1 ここでのバイアスを削除 # 隠れ層の前処理
# バッチノーマル化層
bnmeani = hpreact.mean(0, keepdim=True)
bnstdi = hpreact.std(0, keepdim=True)
hpreact = bngain * (hpreact - bnmeani) / bnstdi + bnbias
線形層では、hpreact のバイアスは実際には無用です。なぜなら、後で各神経元から平均を引くため、これらのバイアスは後の計算に影響を与えないからです。
したがって、バッチ正規化層を使用する場合、以前に重み層(線形層や畳み込み層など)がある場合、バイアスを持つ必要はありません。これは悪影響を及ぼすことはなく、単にトレーニングが勾配を得られないだけで、無駄になります。
ネットワークトレーニングの全体的な構造を整理します:
# バッチ正規化のパラメータとバッファ
bngain = torch.ones((1, n_hidden))
bnbias = torch.zeros((1, n_hidden))
bnmean_running = torch.zeros((1, n_hidden))
bnstd_running = torch.ones((1, n_hidden))
max_steps = 200000
batch_size = 32
lossi = []
for i in range(max_steps):
# ミニバッチを構築
ix = torch.randint(0,Xtr.shape[0],(batch_size,), generator=g)
Xb, Yb = Xtr[ix], Ytr[ix] # バッチ X, Y
# フォワードパス
emb = C[Xb] # 埋め込み
embcat = emb.view(emb.shape[0], -1) # すべての埋め込みベクトルを結合
# 線形層
hpreact = embcat @ W1 # + b1 # 隠れ層の前処理
# バッチノーマル化層
# -------------------------------------------------------------
bnmeani = hpreact.mean(0, keepdim=True)
bnstdi = hpreact.std(0, keepdim=True)
hpreact = bngain * (hpreact - bnmeani) / bnstdi + bnbias
with torch.no_grad():
bnmean_running = 0.999 * bnmean_running + 0.001 * bnmeani
bnstd_running = 0.999 * bnstd_running + 0.001 * bnstdi
# -------------------------------------------------------------
# 非線形性
h = torch.tanh(hpreact) # 隠れ層
logits = h @ W2 + b2 # 出力層
loss = F.cross_entropy(logits, Yb) # 損失関数
# バックワードパス
for p in parameters:
p.grad = None
loss.backward()
# 更新
lr = 0.1 if i < 100000 else 0.01
for p in parameters:
p.data += -lr * p.grad
# 統計を追跡
if i % 10000 == 0:
print(f'{i:7d}/{max_steps:7d}: {loss.item():.4f}')
lossi.append(loss.log10().item())
resnetの構造を例にとると:
ここでの conv 畳み込み層は、前に使用した線形層と基本的に同じですが、畳み込み層は画像に使用されるため、空間構造があります。一方、線形層はブロック化された画像データを処理し、全結合層には空間的概念がありません。
基本的な構造は、私たちが見たものと同じで、重み層、正規化層、非線形性の 3 つの要素から構成されています。
まとめ(省流)#
活性化、勾配、およびそれらの神経ネットワーク内での統計的特性を理解することは非常に重要です。特に神経ネットワークが大きく深くなるときに。
前述の内容を振り返ると:
-
初期化修正:過度に自信のある誤った予測がある場合、最終層の活性化は非常に混乱し、hockey stick の損失関数で終わる可能性があります。この問題を解決することで損失が改善され、無駄なトレーニングを省くことができます。
-
重みの初期化:後に、活性化の分布を制御する必要があることがわかりました。活性化が 0 または無限大に圧縮されることを望んでいません。神経ネットワーク内のすべてのものが均一で、ガウス分布に近いことを望んでいます。等価な問題は「重み行列と初期化プロセス中のバイアスをどのようにスケーリングするか」です。しばらくはテーブルを参照して重みとバイアスを正確に定義できます。
-
バッチ正規化:しかし、ネットワークが大きく深くなると、正規化層を導入する必要があります。その中で最初に登場したのがバッチ正規化です。基本的な考え方は、ほぼガウス分布を得たい場合、平均と標準偏差を取り、データを中心にすることです。
-
バッチ正規化の推論問題:平均と標準偏差の推定をトレーニング部分に置くことで、バッチ正規化層が得られ、神経ネットワーク内の活性化の統計的特性を非常に効果的に制御できます。しかし、誰もこの層を好まないため、バグを引き起こすことが多く、他の代替手段(グループ正規化や層正規化など)を選択することをお勧めします。
Adam のような高度な最適化器や残差接続(Residual connection)を使用する場合、神経ネットワークのトレーニングは基本的にこのようになります。各ステップを非常に正確に保証する必要があり、初期化から活性化と勾配を考慮する必要があります。この時点で非常に深いネットワークをトレーニングすることは不可能です。
補足 1:なぜ tanh 層が必要なのか#
なぜそれらを含める必要があり、ゲインを考慮する必要があるのでしょうか?
答えは簡単です。もし線形層だけがあれば、非常に良い活性化を得るのは簡単ですが、全体としては単なる線形層であり、どんなに重ねても線形変換しか得られません。
tanh の非線形性は、この「線形のサンドイッチ」を線形関数から任意の関数を近似できる神経ネットワークに変えるのです。
補足 2:学習率の設定#
Andrej Karpathy は、学習率が適切に設定されているかどうかを確認するための経験的な方法を提案しました。この方法は、勾配更新とパラメータ値自体の比率を計算することに関係しています。この比率は「ratio」と呼ばれます。
ここでの「ratio」は、単一のパラメータ更新におけるパラメータの変化量と現在のパラメータ値の比率を指します。通常、この比率は、パラメータ更新のステップ(すなわち、勾配に学習率を掛けたもの)とパラメータ値の絶対値との比率を計算することで得られます。たとえば、パラメータとその勾配があり、学習率がの場合、単一の更新の「ratio」は次のように表されます:
この比率が合理的な範囲、たとえば - 3 の近くにある場合、これは学習率が適切に設定されていることを意味します。すなわち、パラメータの更新が過度にならず、また、トレーニングプロセスが遅く無効になることもありません。この範囲は、パラメータの各更新が穏やかであり、学習を促進しつつ、過度の更新ステップによってトレーニングが不安定になることを防ぎます。
実際の操作では、この「ratio」を使用して学習率を監視および調整できます。この比率が大きすぎる場合は、学習率を減少させる必要があります。逆に、この比率が小さすぎる場合は、学習率を増加させることを試みることができます。これは、研究者や実践者が神経ネットワークモデルをデバッグするのに役立つ経験則的な方法です。