Dropout은 두 아이디어를 붙여놓은 것이다: 학습 때 유닛을 무작위로 꺼버리고, 추론 때는 조용히 전부 다시 켠다. 흥미로운 지점은 (1) 유닛을 무작위로 끄는 게 왜 일반화를 개선하는가, 그리고 (2) “전부 다시 켠다"가 공짜가 아니라는 것 — 보정을 안 하면 activation이 잘못된 스케일로 나온다. 이 스케일링을 틀리는 건 딥러닝에서 가장 흔한 버그 중 하나라, 이 글은 둘 다 L1/L2 글과 같은 엔지니어링 깊이로 짚는다.
Dropout이 하는 일
학습 중 dropout layer는 각 유닛의 activation \(a\)를, 독립적으로, keep probability \(p\)로 유지하거나 \(1-p\)로 0으로 만든다. 형식적으로는 유닛마다 mask \(m \sim \text{Bernoulli}(p)\)를 뽑아 \(m \cdot a\)를 출력한다.
$$ m_i \sim \text{Bernoulli}(p), \qquad \tilde a_i = m_i \, a_i $$Forward pass마다 새 mask를 뽑으므로, 각 minibatch는 사실상 무작위로 솎아낸 서로 다른 network를 학습시킨다. Backprop은 살아남은 유닛으로만 흐르고, 그 step에 dropped된 유닛은 gradient가 0이다.
표기 주의. 이 글은 \(p\)를 keep probability로 쓴다(즉 \(\mathbb{E}[m]=p\)). 많은 프레임워크는 대신 drop probability로 매개변수화한다 — PyTorch의
nn.Dropout(p)는 각 원소를 확률 \(p\)로 0으로 만들므로, 그쪽의 keep probability는 \(1-p\)다. 같은 메커니즘, 변수만 뒤집힘. 공식 읽을 때 주의.
메커니즘은 이게 전부다. 아래는 전부 그 결과다.
왜 일반화에 도움이 되나
이유가 하나가 아니다. Dropout은 여러 독립적인 각도에서 동시에 좋아 보이는 종류의 방법이다. 실제로 그 동작을 설명하는 관점들을 보자.
1. Co-adaptation을 깨뜨린다
Dropout이 없으면 hidden unit은 다른 특정 유닛이 존재할 때만 작동하는 feature를 학습할 수 있다 — 한 유닛이 다른 유닛의 실수를 메우고, 둘이 함께 아주 특정하고 취약한 패턴을 인코딩하는데, 이게 흔히 training 노이즈를 외운 결과다. 이것이 co-adaptation이다.
Dropout은 이 전략을 불가능하게 만든다. 어떤 유닛이든 다음 step엔 사라질 수 있으니, 어떤 유닛도 특정 동료가 있으리라고 기댈 수 없다. 각 유닛은 어떤 맥락이 살아남든 거기에 robust하게, 어느 정도 혼자서도 쓸모 있는 feature를 학습하도록 강제된다. 결과는 더 중복적이고(redundant) 더 독립적으로 의미 있는 feature이고, 바로 이게 처음 보는 데이터로 잘 전이된다. Hinton 등의 원래 동기였고, 효과를 이해하는 가장 직관적인 방법이다.
2. \(2^n\)개 subnetwork의 암묵적 ensemble
Dropout 대상 유닛이 \(n\)개인 layer는 가능한 mask가 \(2^n\)개다 — 그만큼의 서로 다른 “솎인” subnetwork. 결정적으로, 이들은 전부 같은 weight를 공유한다. 각 학습 step은 subnetwork 하나를 뽑아, 공유 weight를 그 subnetwork가 조금 더 나아지도록 민다.
학습 내내 당신은 weight sharing으로 엮인 지수적으로 많은 network의 ensemble을 함께 fitting하는 셈이다. Ensemble은 고전적인 variance 감소 기법이다: 서로 다른 방식으로 overfit하는 여러 모델을 평균 내면 각자의 특유한 오차가 상쇄된다. Dropout은 \(2^n\)개 모델을 학습·저장하지 않고 이 이득을 얻는다 — weight sharing이 ensemble 전체를 하나의 파라미터 집합으로 접어버린다. 뒤에 보겠지만 test-time 동작은 정확히 이 ensemble의 예측을 평균 내는 것의 근사이고, 그게 이 관점을 아래 스케일링 트릭과 연결한다.
3. 곱셈 노이즈 ≈ adaptive L2 penalty
Activation에 Bernoulli 노이즈를 곱하는 건, 말 그대로 hidden representation에 노이즈를 주입하고도 답을 맞히라고 요구하는 것이다. 노이즈에 robust하도록 학습하는 것은 regularizer다.
이건 정밀하게 만들 수 있고, L1/L2 글로 곧장 연결된다. Input에 dropout을 적용한 linear model(또는 GLM)에서 dropout 노이즈에 대해 loss를 평균 내면, 기대 objective가 원래 loss 더하기 penalty 형태로 정리되는데:
$$ R(\mathbf{w}) \;\propto\; \frac{1-p}{p}\sum_j \big(\text{feature } j \text{의 scale}\big)\, w_j^2 . $$이건 ridge (L2) penalty다 — 단, 각 weight에 걸리는 강도가 그 feature 고유의 scale/variance에 자동으로 비례한다. 즉 dropout은 정규화된 feature에 적용한 ridge처럼 동작한다: 드물게 켜지거나 scale이 작은 feature가 과소 penalty받지 않고, 지배적인 feature가 과대 penalty받지 않도록 penalty를 스스로 조정한다. Keep rate \(p\)는 \(1/\lambda\) 역할을 한다: \(p\)가 작을수록(더 공격적으로 drop) \(\frac{1-p}{p}\)가 커지고 ⟹ 규제가 강해진다. (Wager, Wang & Liang, 2013이 이 동치를 유도했다.)
Bias–variance 요약
세 관점 모두 bias–variance 분해의 언어로는 같은 거래다: 뽑힌 각 subnetwork는 개별적으로 더 약하지만(bias 약간 증가), robust·redundant feature를 강제하고 ensemble을 평균 내면서 variance가 크게 준다. 총 test error가 내려간다. Dropout은 L1/L2가 놓인 것과 같은 곡선 위의 노브이고, 다만 명시적 penalty 항 대신 stochastic masking으로 구현됐을 뿐이다.
학습 vs 추론: 스케일링 문제
모두가 언젠가 걸려 넘어지는 지점이다.
학습 때, dropout 하류의 유닛은 모든 유닛이 켜졌을 때 받을 신호의 평균적으로 \(p\) 비율만 받는다 — 들어오는 각 activation이 확률 \(p\)로만 존재하기 때문이다:
$$ \mathbb{E}[\tilde a_i] = \mathbb{E}[m_i]\, a_i = p \, a_i . $$그래서 모든 layer는 기대 크기가 \(p\)로 스케일된 입력에 맞춰 weight를 학습한다.
추론 때는 무작위 예측을 원하지 않으니 masking을 멈추고 전체 network(모든 유닛 켜짐)를 쓴다. 그런데 이제 각 유닛이 자신의 full activation \(a_i\)를 하류로 넘긴다 — 다음 layer가 학습 때 기대하던 \(p\,a_i\)보다 대략 \(1/p\)배 크다. 이후 모든 layer가 잘못된 scale의 입력을 보고, 분포가 어긋나고, 출력이 엉망이 된다. 이 \(p\) 인자를 보정해야 한다.
보정을 넣을 자리가 두 곳 있다.
Classic dropout: test 때 스케일 다운
학습은 위 그대로(출력 \(m_i a_i\), 스케일링 없음). Test 때 activation에 \(p\)를 곱해, 결정론적 activation이 학습 때의 기댓값과 맞도록 한다:
$$ a_i^{\text{test}} = p \, a_i = \mathbb{E}_{\text{train}}[m_i a_i]. \quad\checkmark $$동치로 나가는 weight에 \(p\)를 곱해도 된다. 작동은 하지만, 추론 코드가 \(p\)에 의존하게 되고 평범한 network와 달라진다.
Inverted dropout: train 때 스케일 업 (현대의 기본값)
대신 보정을 학습 때 하는데, 살아남은 activation을 \(p\)로 나눈다:
$$ \tilde a_i = \frac{m_i \, a_i}{p}, \qquad \mathbb{E}[\tilde a_i] = \frac{p \, a_i}{p} = a_i . $$이제 학습 때 기댓값이 이미 평범한, 스케일 안 한 activation과 같아진다 — 그래서 추론 때는 아무것도 안 한다: 모든 유닛 켜고 \(a_i\)를 그대로 쓴다. PyTorch, TensorFlow 등이 실제로 구현하는 방식이고, 기본값인 데엔 좋은 이유가 있다:
- 추론 코드가 dropout 없는 network와 동일하다. Eval 모드에서 dropout layer는 말 그대로 no-op(identity)다. 배포 쪽은 \(p\)를 알 필요가 없다.
- Dropout rate가 추론과 분리된다. \(p\)를 바꾸거나 dropout을 완전히 꺼도 추론 코드는 한 줄도 안 건드린다.
- 추가 비용이 전부 학습 때 산다. 이미 비싼 계산을 하는 곳이라 부담이 적고, 추론 경로는 최대한 단순·빠르게 유지된다.
| Classic dropout | Inverted dropout (기본값) | |
|---|---|---|
| Train 출력 | \(m_i \, a_i\) | \(m_i \, a_i \,/\, p\) |
| Test 출력 | \(p \, a_i\) (스케일 다운) | \(a_i\) (변화 없음) |
| \(1/p\)가 사는 곳 | test 때 | train 때 |
| 추론 코드 | \(p\)에 의존 | dropout 없을 때와 동일 |
수치 예시 (\(p = 0.5\), 즉 절반을 drop): inverted dropout에서는 살아남은 각 activation이 학습 때 \(1/p = 2\)배 곱해지므로, 다음 layer로 가는 기대 총신호가 보존된다. Test 때는 activation을 스케일 없이 그대로 흘린다. Classic dropout이라면 반대로 학습은 그대로 두고 test 때 모든 activation을 절반으로 줄인다.
model.eval()버그. 학습과 추론의 동작이 다르므로 프레임워크가 지금 어느 모드인지 알아야 한다. PyTorch에서는model.eval()(다시 켜려면model.train())이 그 역할로, Dropout을 identity 경로로(그리고 BatchNorm을 running stats로) 전환한다. 추론 때model.eval()을 빼먹는 건 고전적 버그다: dropout이 계속 켜져 유닛을 무작위로 0으로 만들어, 예측이 노이즈 끼고 나빠지며 실행마다 달라진다. Eval 정확도가 영문 모르게 낮고 들쭉날쭉하면 여기부터 확인하라.
왜 스케일링이 (선형 부분에 대해) 정확히 ensemble 평균인가
이 스케일링은 임의의 땜질이 아니라, 단 한 번의 결정론적 forward pass가 관점 #2의 ensemble을 근사하게 만드는 장치다. 진짜 ensemble 예측은 mask에 대해 평균 낸 \(\mathbb{E}_m[f(\tilde{\mathbf a})]\)다. 임의의 선형 연산에서는 기댓값이 그대로 통과한다: \(\mathbb{E}_m[\tilde{\mathbf a}]\cdot W = \mathbf a \cdot W\)가 정확히 성립한다(바로 \(1/p\) 스케일링이 맞춰주는 것). 그래서 결정론적 full-network pass는 network를 기대 activation에서 평가한 값을 계산하는데, 이는 선형 부분에서는 ensemble 평균과 정확히 같고 비선형을 지나면서는 그것을 가깝게 근사한다(Jensen 부등식 때문에 정확하진 않지만 경험적으로 딱 맞는다). Srivastava 등은 이 결정론적 pass가 사실상 \(2^n\)개 subnetwork 출력 분포의 정규화된 기하평균을 계산함을 보였다 — 단일 선형/softmax layer에서는 정확히, 깊은 network에서는 근사적으로. 스케일링과 ensemble 관점은 같은 사실이다.
실전 노트
- 전형적 rate. 원 논문 기본값은 fully-connected hidden layer에 절반 drop(\(p=0.5\) keep), input에는 더 가볍게(keep \(0.8\)–\(0.9\)). 더 많이 drop = 더 강한 규제. 다른 규제 노브처럼 튜닝하라.
- Convolution layer. 평범한 dropout은 conv feature map에서 약하다 — 이웃 activation이 공간적으로 correlated라, 한 픽셀을 0으로 만들어도 이웃이 여전히 그 정보를 갖고 있기 때문이다. SpatialDropout(채널 통째로 drop)과 DropBlock(연속 영역 drop)이 correlated 그룹을 한 번에 제거해 이를 해결한다.
- BatchNorm과의 상호작용. BatchNorm 자체가 강한 regularizer라, 그 앞에 dropout을 쌓으면 train/test 간 “variance shift"가 생겨 해가 될 수 있다(Li 등, 2019). 많은 현대 CNN(예: ResNet)은 BatchNorm에 기대고 dropout은 거의 안 쓴다. 둘을 같이 쓸 땐 순서와 위치가 중요하다.
- Transformer는 dropout을 많이 쓴다 — attention weight, residual branch, embedding에 — 이 layer들이 넓고 대부분 fully-connected라 overfit하기 쉽고, conv식 공간 상관 주의사항이 적용되지 않기 때문이다.
- MC Dropout(버그가 아니라 기능). 일부러 추론 때 dropout을 켜두고 forward pass를 여러 번 돌리면, 예측의 퍼짐이 모델 불확실성을 추정한다(Gal & Ghahramani, 2016). Test 때 dropout을 켜두는 게 옳은 유일한 경우이고 — 말 그대로
model.eval()버그를 의도적으로 재활용한 것이다.
요약
- 왜 일반화되나. Dropout은 유닛이 취약한 공동 feature로 co-adapt하는 걸 막고, weight를 공유하는 \(2^n\)개 subnetwork의 암묵적 ensemble을 학습하며, adaptive L2 penalty처럼 작용하는 곱셈 노이즈를 주입한다. Bias–variance로는: subnetwork당 bias는 약간 늘지만 variance가 훨씬 줄어 — 순이득.
- 학습 vs 추론. 학습은 유닛을 mask하므로 하류 activation이 기댓값에서 \(p\)로 스케일된다. 추론은 전체 network를 쓰므로 그 \(p\) 인자를 보정하지 않으면 모든 layer가 잘못된 입력 scale을 본다.
- 스케일링 트릭. Inverted dropout(기본값)은 학습 때 살아남은 activation을 \(p\)로 나눠, 추론을 평범한·스케일 없는·모든 유닛 켠 forward pass로 만든다 — dropout 없는 network와 동일하다. 프레임워크가 실제로 그 경로로 전환하도록
model.eval()을 잊지 마라.