pytorchのdropoutって実際どう動いてる?
背景:Transformerベースの距離学習を行う際に、自作したモデルの学習がかなり不安定
前提として、今回使用しているのは、pytorchのnn.Moduleにあるサブクラス(nn.linearで線形層が作れる)で自作したTransformerについてである。
現在TransformerベースのLLMやViTでは、汎化性能向上と過学習回避のためにdropoutという機能が当たり前に追加されている。
Dropoutとは
ニューラルネットワークの学習において、活性化しているニューロンの一部をランダムに不活性化(マスク)することで、
「一部の情報が欠けていてもタスクを解けるように学習させる」手法。
これにより、特定のニューロンに重みが集中することを防ぎ、汎化性能の向上が期待できるというもの。
学習不安定性について自分のコードを見つめていると、自作したTransformerにdropoutを追加していなかった...
じゃあdropoutを追加するとして、ベクトルベースでどう処理してるんだ?と素朴な疑問が出てきた。
ニューロンといわれてもなかなかぴんと来ない、というかベクトル化、embeddingの研究者としてベクトル計算がどうされているのかで理解したいと思い、今回記事にまとめてみたいと思います。
code:example
x = torch.tensor(0.5, 1.0, -0.5)
dropout = nn.Dropout(p=0.3)
output = dropout(x)
例えば、xのような三次元のベクトルがあったときに、p=0.3で設定したdropoutに入力すると、outputには
各次元の値に対して別々に、pの確率で0、1-pの確率でそのままの値が保持される
つまり、三次元のベクトルであれば、三回の試行があり、一次元目の値に対して30%の確率で0、70%で0.5の値が保持、二次元目の値に対して...という感じで処理が進んでいく。
なので、全ての次元が0になることも、ありえなくはないわけだ
詳細処理を以下にまとめる
内部の処理についてポイントは二つ
・マスクによる不活性化
・スケーリング補正
code:dropout
#dropoutの中身の処理のイメージ
def dropout(x, p=0.3, training=True):
if not training or p == 0.0:
return x
mask = (torch.rand_like(x) > p).float() # 各要素ごとにBernoulli(1-p)
return mask * x / (1 - p)
#今回の例の場合
#マスクによる不活性化
mask = 1, 0, 1 #三次元のベクトルに対する30%の確率で0、70%の確率で1を算出しマスクを用意
#これをxに掛けることで二次元目の値が0、それ以外の次元の値は保持されるという仕組み
x * mask #0.5, 0.0, -0.5になる
#スケーリング補正
output = (x * mask) / (1 - p)
#残った要素の値を (1 /(1 - p)) 倍して期待値(平均の大きさ)を保つようにスケーリング補正
#0.7で各要素を割る
print(output)
→0.71, 0.00, -0.71
スケーリングがなぜ必要かというと、推論時にdropoutは機能しないので
学習時と推論時の出力のスケールが変わってしまうことで性能低下につながってしまうから!
ニューロン単位で不活性処理を加えているというのが、ベクトルの各次元に対して独立にpで不活性処理を行っているということと同義であるということを改めて理解できた。
#Yuro_Kanada