딥러닝을 공부하다 보면 Dropout을 사용한 후 출력 결과를 보고 의문이 생기는 경우가 많습니다.
예를 들어 다음과 같은 코드를 실행해 보겠습니다.
import torch
import torch.nn as nn
inputs = torch.randn(4, 5)
m = nn.Dropout(p=0.2)
output = m(inputs)
print(inputs)
print(output)
실행 결과는 다음과 비슷하게 나타납니다.
inputs
tensor([[-0.8982, -0.4609, -0.2699, -1.1008, -0.4742],
[-1.5193, -0.3182, -1.6212, -0.6010, -0.1831],
[-2.1527, -0.1178, 1.1964, -1.5621, 1.2346],
[-0.3991, -0.4037, 2.1003, -1.6789, 0.7428]])
output
tensor([[-1.1228, -0.5762, -0.3373, -1.3760, -0.5928],
[-1.8991, -0.3978, -2.0265, -0.7513, -0.2289],
[-2.6909, -0.0000, 1.4955, -0.0000, 0.0000],
[-0.0000, -0.5046, 2.6253, -2.0986, 0.9284]])
처음 보면 대부분 이렇게 생각합니다.
“Dropout은 일부 값을 0으로 만드는 것 아닌가? 그런데 왜 나머지 값도 모두 달라졌을까?”
이번 글에서는 이러한 의문을 하나씩 해결해 보겠습니다.
Dropout이란?
Dropout은 학습 과정에서 일부 뉴런(Neuron)을 무작위(Random)로 제거하여 특정 뉴런에 과도하게 의존하는 것을 방지하고, 과적합(Overfitting)을 줄이는 대표적인 정규화(Regularization) 기법입니다.
예를 들어
nn.Dropout(p=0.2)
이라고 하면 많은 사람들이
- 20%는 0으로 변경
- 나머지 80%는 그대로 유지
라고 생각합니다.
하지만 실제 PyTorch는 조금 다르게 동작합니다.
살아남은 값도 모두 변경된다
PyTorch는 Inverted Dropout 방식을 사용합니다.
즉,
- Drop된 값 → 0
- 살아남은 값 → 1 / (1-p) 만큼 자동으로 확대
공식은 다음과 같습니다.
예를 들어
p = 0.2
이면
즉 살아남은 모든 값은 1.25배가 됩니다.
실제 예제로 확인하기
첫 번째 원소를 살펴보겠습니다.
입력
-0.8982
출력
-1.1228
계산하면
-0.8982 × 1.25
=
-1.12275
실제 출력과 거의 동일합니다.
다른 값도 마찬가지입니다.
| 입력(Input) | 출력(Output) |
|---|---|
| -0.4609 | -0.5762 |
| -0.2699 | -0.3373 |
| -1.1008 | -1.3760 |
모두 1.25배가 된 것을 확인할 수 있습니다.
Drop된 값은 어떻게 될까?
세 번째 행을 보면
입력
[-2.1527, -0.1178, 1.1964, -1.5621, 1.2346]
출력
[-2.6909, 0, 1.4955, 0, 0]
이를 표로 정리하면
| 입력 | Drop 여부 | 출력 |
|---|---|---|
| -2.1527 | 유지 | -2.6909 |
| -0.1178 | Drop | 0 |
| 1.1964 | 유지 | 1.4955 |
| -1.5621 | Drop | 0 |
| 1.2346 | Drop | 0 |
Drop되지 않은 값은 모두 1.25배가 되었고, Drop된 값은 0이 되었습니다.
p=0.2인데 왜 항상 20%가 Drop되지 않을까?
많은 분들이 nn.Dropout(p=0.2)를 보면
“항상 정확히 20%가 제거되겠구나.”
라고 생각합니다.
하지만 실제로는 그렇지 않습니다.
예를 들어 현재 입력의 크기는
4 × 5 = 20개 원소
입니다.
이론적으로는
20 × 0.2 = 4개
가 Drop될 것 같지만 실제 실행 결과는
- 첫 번째 실행 → 3개 Drop
- 두 번째 실행 → 5개 Drop
- 세 번째 실행 → 2개 Drop
- 네 번째 실행 → 6개 Drop
처럼 매번 달라질 수 있습니다.
그 이유는?
Dropout은 전체 행렬에서 정확히 20%를 선택하는 방식이 아닙니다.
각 원소가 독립적으로 Drop될지를 결정합니다.
즉 각 원소마다
- 20% 확률로 Drop
- 80% 확률로 유지
라는 Bernoulli Trial(베르누이 시행) 을 수행합니다.
따라서 어떤 원소가 Drop되는지는 서로 영향을 주지 않습니다.
중요한 점은
p는 제거되는 비율(Ratio)이 아니라, 각 뉴런이 제거될 확률(Probability)을 의미합니다.
즉,
p=0.2
≠ 항상 20% 제거
p=0.2
= 각 원소가 20% 확률로 제거
입니다.
입력 데이터가 작을수록 실제 Drop 비율의 변동은 커지고,
입력 데이터가 많아질수록 실제 Drop 비율은 20%에 가까워집니다.
평균적으로는 20%가 제거된다
이를 직접 확인해 보겠습니다.
import torch
import torch.nn as nn
inputs = torch.ones(1000)
m = nn.Dropout(p=0.2)
drop_count = 0
for _ in range(1000):
output = m(inputs)
drop_count += (output == 0).sum().item()
print(drop_count / (1000 * 1000))
실행 결과
0.1998
또는
0.2003
처럼 0.2에 매우 가까운 값이 나옵니다.
즉,
한 번의 Dropout에서는 정확히 20%가 아니지만,
충분히 많이 반복하면 평균적으로는 20%가 제거됩니다.
왜 살아남은 값을 1.25배 할까?
그 이유는 Dropout 적용 후에도 입력값의 기댓값(Expected Value)을 원래 값과 동일하게 유지하기 위해서입니다.
예를 들어 입력값이 10이고, Dropout 확률이 p = 0.2라고 가정해 보겠습니다.
Dropout에서는 학습 과정에서:
- 20% 확률 → 뉴런 제거 (0)
- 80% 확률 → 뉴런 유지
가 됩니다.
그런데 단순히 유지된 값을 그대로 사용하면:
- 20% 확률: 0
- 80% 확률: 10
이므로 평균적인 출력값은
이 됩니다.
즉, Dropout 때문에 출력의 평균 크기가 10 → 8로 감소하게 됩니다.
이를 방지하기 위해 살아남은 값에 1/(1-p) 만큼 보정합니다.
Dropout 확률이 0.2이면:
따라서 살아남은 값은:
가 됩니다.
그러면:
- 20% 확률 → 0
- 80% 확률 → 12.5
이고, 기댓값은:
이 됩니다.
즉, Dropout을 적용해도 평균적으로 입력값의 크기(기댓값)는 그대로 유지됩니다.
즉 평균적으로는 원래 입력과 동일한 값을 유지하게 됩니다.
그래서 학습 과정에서도 출력의 평균적인 크기가 변하지 않습니다.
그런데 mean()은 왜 달라질까?
많은 분들이 다음과 같이 확인합니다.
print(inputs.mean())
print(output.mean())
예를 들어
inputs.mean()
tensor(-0.4244)
output.mean()
tensor(-0.4777)
를 보고
“기댓값이 같다고 했는데 평균은 왜 다르지?”
라고 생각합니다.
하지만 여기서 계산한 것은 기댓값(Expected Value) 이 아니라
현재 행렬의 평균(mean) 입니다.
즉,
inputs.mean()
은 현재 입력 행렬의 평균이고,
output.mean()
은 한 번의 Dropout 결과의 평균입니다.
Dropout은 무작위 연산이므로 실행할 때마다 평균도 조금씩 달라지는 것이 정상입니다.
기댓값은 어떻게 확인할까?
기댓값은 한 번의 결과만으로는 확인할 수 없습니다.
같은 입력에 대해 Dropout을 매우 많이 반복한 후 평균을 계산해야 합니다.
outputs = []
for _ in range(100000):
outputs.append(m(inputs).mean())
print(torch.stack(outputs).mean())
예를 들어
inputs.mean()
-0.4244
100000번 평균
-0.4245
처럼 입력 평균과 거의 동일한 결과를 얻을 수 있습니다.
반복 횟수를 늘릴수록 두 값은 더욱 가까워집니다.
이는 대수의 법칙(Law of Large Numbers) 에 의해 설명됩니다.
즉,
- 한 번의 Dropout 결과는 입력과 다를 수 있다.
- 하지만 충분히 많이 반복하면 평균은 입력 평균과 거의 같아진다.
이를 통해 Dropout이 기댓값을 유지한다는 것을 직접 확인할 수 있습니다.
train()과 eval()의 차이
Dropout은 학습과 추론에서 다르게 동작합니다.
학습 모드
model.train()
- 일부 뉴런 제거
- 살아남은 값은 자동으로 1/(1-p)배 확대
평가 모드
model.eval()
Dropout은 비활성화됩니다.
즉
output = input
과 동일하게 동작합니다.
마무리
PyTorch의 nn.Dropout은 단순히 일부 값을 0으로 만드는 기능이 아닙니다.
- 일부 뉴런을 무작위(Random) 로 제거한다.
- 제거되지 않은 뉴런은 1/(1-p) 만큼 자동으로 확대한다.
- 이는 출력의 기댓값(Expected Value) 을 유지하기 위한 Inverted Dropout 방식이다.
p는 제거 비율(Ratio) 이 아니라 각 뉴런이 제거될 확률(Probability) 을 의미한다.- 따라서 한 번의 실행에서는 정확히 20%가 제거되지 않을 수 있다.
inputs.mean()과output.mean()은 한 번의 샘플 평균이므로 서로 다를 수 있다.- 같은 입력에 대해 Dropout을 충분히 많이 반복하면 평균은 입력 평균과 거의 같아지며, 이를 통해 Dropout이 기댓값을 유지한다는 것을 확인할 수 있다.
model.train()에서는 Dropout이 적용되고,model.eval()에서는 Dropout이 비활성화된다.
Dropout의 동작 원리를 이해하면 왜 0이 아닌 값도 변경되는지, 왜 매번 Drop되는 개수가 달라지는지, 그리고 왜 평균(mean)은 달라질 수 있지만 기댓값(Expected Value)은 유지되는지를 훨씬 명확하게 이해할 수 있습니다.

