지난 글에서 배치 정규화와 레이어 정규화가 활성값의 분포를 고르게 만들어 학습을 안정시킨다는 것을 봤다. 이번 글은 같은 정규화 계열이지만 접근이 정반대인 기법을 다룬다. 분포를 고르게 만드는 대신 일부러 망가뜨리는 것이다. 2014년 Srivastava et al.이 발표한 드롭아웃(Dropout)은 훈련 중 뉴런 일부를 무작위로 꺼 버린다. 아이디어가 이렇게 단순한데도 과적합 방지에 잘 듣고, 구현은 몇 줄이며, 지금도 트랜스포머 블록 안에 그대로 들어 있다. 왜 망가뜨리는 것이 도움이 되는지, 그리고 그 과정에서 무엇이 어긋나 보정이 필요한지를 계산으로 따라가 보자.
무작위 비활성화
학습과 추론의 비대칭
드롭아웃은 훈련할 때만 하는 일이다. 미니배치 하나를 흘릴 때마다 각 뉴런을 확률 로 꺼서, 다시 말해 그 출력을 0으로 바꿔서 이번 순전파에 없던 것처럼 만든다. 추론 때는 아무것도 끄지 않고 전부 쓴다.
여기서 "끈다"는 말이 무엇을 뜻하는지 분명히 해 둘 필요가 있다. 뉴런이 사라지는 것도, 그 가중치가 지워지는 것도 아니다. 이번 순전파에서 그 출력값을 0으로 두는 것뿐이고, 그러면 역전파에서도 그 경로로 기울기가 흐르지 않아 이번 스텝의 갱신에서 빠진다. 다음 미니배치에서는 다시 살아날 수 있다. 꺼지는 것은 뉴런이 아니라 이번 스텝의 그 값이다.
같은 입력을 두 번 넣으면 훈련 모드에서는 다른 값이 나오고 추론 모드에서는 같은 값이 나온다. 이 비대칭이 드롭아웃과 관련된 거의 모든 실수의 뿌리다. PyTorch에 model.train()과 model.eval()이 따로 있는 이유의 절반이 여기 있다 — 나머지 절반이 지난 글의 배치 정규화다.
import torch
import torch.nn as nn
# 기본 사용
dropout = nn.Dropout(p=0.5) # p: 비활성화 확률
x = torch.ones(3, 10) # 배치 크기 3, 특성 10
# 훈련 모드: p=0.5로 랜덤 마스킹
dropout.train()
out_train = dropout(x)
print(out_train)
# tensor([[0., 2., 0., 2., 0., 2., 0., 2., 0., 2.],
# [2., 0., 2., 0., 2., 0., 2., 0., 2., 0.],
# ...])
# → 약 50%가 0, 나머지는 1/(1-0.5)=2로 스케일
# 추론 모드: 모든 뉴런 활성, 스케일 없음
dropout.eval()
out_eval = dropout(x)
print(out_eval)
# tensor([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], ...])
공적응
끄는 것이 왜 도움이 되는가. 원 논문이 내놓은 설명은 공적응(co-adaptation)이다. 어떤 뉴런이 "옆자리 뉴런이 큰 값을 낼 때만 내가 의미 있는 값을 낸다"는 식으로 특정 짝에 기대 버릇을 들이는 현상을 가리킨다. 이렇게 짜인 조합은 훈련 데이터에서만 성립하는 우연일 때가 많다. 훈련 중에 짝이 언제 사라질지 모르는 상황에서는 그런 의존을 만들 수 없으므로, 각 뉴런은 혼자서도 쓸 만한 특징을 배우게 된다.
이것이 과적합을 막는 방식이 된다. 과적합은 모델이 데이터에 담긴 규칙 대신 그 데이터에만 있는 잡음까지 외우는 것인데, 잡음을 외우려면 대개 여러 뉴런이 정확히 맞물려 돌아가야 한다. 규칙은 몇 개가 빠져도 남지만 외운 조합은 한 자리만 비어도 무너진다. 드롭아웃은 매 스텝 그 조합을 무작위로 깨뜨려 후자를 배우기 어렵게 만든다.
같은 일을 잡음의 관점에서 볼 수도 있다. 뉴런을 끄는 것은 활성값에 곱셈 형태의 잡음을 넣는 일이고, 잡음이 섞인 입력에서도 같은 답을 내려면 모델은 한 자리에 몰리지 않고 여러 자리에 나눠 근거를 두어야 한다. 두 설명은 같은 현상을 다른 쪽에서 본 것이다.
확률 p
는 끄는 확률이다. 살리는 확률이 아니다. 이것이 헷갈리기 쉬운 자리인 이유는 역사에 있다 — 원 논문과 텐서플로 1.x의 keep_prob은 살리는 확률이었고, PyTorch의 nn.Dropout(p=0.5)와 케라스의 Dropout(0.5)는 끄는 확률이다. 같은 0.8이 어떤 코드에서는 거의 다 살리는 설정이고 다른 코드에서는 거의 다 죽이는 설정이라는 뜻이다. 남의 코드에서 값을 옮겨 올 때 먼저 확인할 것이 이것이다.
이면 모듈이 있어도 아무 일이 일어나지 않고, 가 1에 가까우면 층이 사실상 통째로 꺼져 학습이 안 된다. 쓰는 범위는 대체로 0.1에서 0.5 사이이고, 어느 값을 어디에 쓰는지는 마지막 절에서 다룬다.
끄는 단위가 무엇인지도 값만큼 중요하다. nn.Dropout은 텐서의 원소 하나하나를 따로 뽑으므로, 같은 미니배치 안에서도 샘플마다 꺼지는 자리가 다르다. 배치 전체에 같은 마스크를 쓰는 구현도 있고 채널이나 블록을 통째로 끄는 변형도 있는데, 무엇을 단위로 삼느냐가 이 기법의 효과를 크게 바꾼다. 뒤의 「변형들」 절이 그 단위를 바꾸는 이야기다.
Inverted Dropout
기대값의 어긋남
끄기만 하고 끝내면 훈련과 추론이 서로 다른 크기의 값을 다음 층에 넘긴다. 숫자로 보자. 어떤 층의 출력 10개가 모두 1이고 다음 층이 그 합을 쓴다고 하자. 의 훈련에서는 평균 5개만 살아남으므로 합의 기대값이 5다. 추론에서는 10개가 다 살아 합이 10이다. 다음 층은 훈련 내내 5 근처의 값을 보다가 추론에서 갑자기 10을 받는다.
층이 여럿이면 이 배율이 층마다 곱해진다. 드롭아웃을 세 층에 걸어 두면 마지막 층이 받는 값은 훈련 때의 여덟 배다. 활성 함수가 비선형이므로 값이 커지면 포화 구간으로 밀려나거나 소프트맥스의 분포가 뾰족해져, 결과는 "조금 빗나간 예측"이 아니라 "쓸 수 없는 예측"이 된다.
1/(1−p) 스케일
원 논문의 보정은 추론 쪽에 있었다. 추론 때 가중치에 를 곱해 크기를 맞추는 방식이다. 지금 쓰는 구현은 그것을 뒤집어 훈련 쪽에서 보정한다. 살아남은 값을 배로 키워 두는 것이고, 이 방식을 Inverted Dropout이라 한다.
기대값을 직접 계산하면 왜 맞아떨어지는지 보인다. 값 는 확률 로 살아남아 가 되고 확률 로 0이 되므로, 기대값은 다. 훈련에서 다음 층이 받는 값의 기대값이 드롭아웃이 없을 때와 같아졌으므로, 추론에서는 아무 보정도 하지 않고 그냥 통과시키면 된다.
뒤집어서 얻는 것이 둘 있다. 하나는 추론 경로가 완전히 비어 있다는 것이다. 추론은 배포되어 수없이 돌고 훈련은 한 번 도는데, 비용을 도는 횟수가 적은 쪽에 몰아 둔 셈이다. 다른 하나는 를 바꿔 다시 학습해도 추론 코드를 건드릴 필요가 없다는 것이다. 으로 두면 스케일이 1이 되어 모듈 자체가 저절로 없는 것처럼 행동한다.
def inverted_dropout(x, p, training=True):
if not training or p == 0:
return x
# Bernoulli 마스크: p 확률로 0 (비활성화)
mask = (torch.rand_like(x) > p).float()
# Inverted: 1/(1-p)로 스케일하여 기대값 유지
return x * mask / (1.0 - p)
# 기대값 확인
x = torch.ones(10000)
out = inverted_dropout(x, p=0.3, training=True)
print(f"기대값: {out.mean():.4f}") # ≈ 1.0 (유지!)
# 추론 시 스케일 보정 필요 없음
out_eval = inverted_dropout(x, p=0.3, training=False)
print(f"추론 기대값: {out_eval.mean():.4f}") # 1.0 (동일)
모드 전환 실수
가장 흔한 사고는 평가 루프에서 모드를 바꾸지 않는 것이다. 증상은 검증 성능이 훈련보다 눈에 띄게 낮고 에폭마다 들쭉날쭉하게 나오는 것인데, 이것만으로는 과적합과 구별이 안 된다. 구별하는 확실한 방법이 하나 있다 — 같은 검증 셋을 연달아 두 번 재 보는 것이다. 데이터도 가중치도 그대로인데 숫자가 다르게 나온다면 원인은 무작위성밖에 없고, 그 무작위성을 만드는 것은 드롭아웃 아니면 배치 정규화다.
반대 방향의 실수도 있다. 검증을 마치고 model.train()으로 되돌려 놓지 않아 나머지 훈련이 드롭아웃 없이 도는 경우다. 이쪽은 증상이 훨씬 조용하다. 훈련 손실은 오히려 더 잘 떨어지고 과적합만 심해지므로, 정규화가 통째로 사라진 것을 모른 채 "이 모델은 원래 과적합이 심하다"고 결론짓기 쉽다. 두 실수 모두 평가 함수를 try / finally로 감싸 모드를 되돌리는 것으로 막을 수 있다.
앙상블 해석
서브네트워크
드롭아웃을 다른 각도에서 보자. 뉴런이 개이고 각각이 켜지거나 꺼진다면 가능한 조합은 가지다. 이 조합 하나하나가 원래 네트워크에서 일부 연결만 남긴 서브네트워크이고, 미니배치마다 그중 하나를 뽑아 한 스텝만큼 학습시키는 것이 훈련 과정이다. 은닉 뉴런이 1,000개면 가지이므로 훈련 내내 같은 서브네트워크를 두 번 뽑는 일은 사실상 없다.
숫자만 보면 말이 안 되는 이야기다. 서브네트워크 하나가 한 스텝씩만 학습된다면 그 하나하나는 학습이 거의 안 된 상태로 남는다. 성립하는 이유는 이들이 가중치를 나눠 쓰기 때문이다. 어떤 스텝에서 뉴런 7번의 가중치를 고치면 7번을 포함하는 모든 서브네트워크가 동시에 그만큼 좋아진다. 그러니 한 번의 갱신이 한 모델을 조금 고치는 것이 아니라 그 뉴런을 쓰는 절반의 모델을 한꺼번에 고치는 셈이고, 개수가 지수로 늘어도 학습이 따라갈 수 있다.
기하평균 근사
그렇다면 추론에서 전부 켜고 한 번 돌리는 것은 무엇에 해당하는가. 이 값이 개 서브네트워크의 예측을 모아 놓은 것에 가깝다는 것이 앙상블 해석의 주장이다. 정확히는 산술평균이 아니라 기하평균의 근사이고, 선형 층 하나에 소프트맥스를 붙인 모델에서는 이 동치가 정확히 성립한다는 것을 원 논문이 보였다.
깊은 비선형 네트워크에서는 근사다. 증명이 기대던 선형성이 층마다 깨지기 때문인데, 실험적으로는 충분히 잘 맞아서 개를 실제로 돌려 평균 내는 것과 큰 차이가 없었다. 다시 말해 앙상블의 이득을 한 번의 순전파 값으로 사는 셈이고, 이것이 드롭아웃이 싼 기법인 이유다.
배깅과의 차이
그렇다고 드롭아웃이 배깅과 같은 것은 아니다. 배깅은 모델마다 파라미터를 따로 두고 데이터도 부분집합을 따로 뽑아 학습시킨다. 드롭아웃의 서브네트워크들은 가중치를 공유하고 모두 같은 데이터를 본다. 공유는 저장 비용을 없애 주는 대신 독립성을 앗아 간다 — 앙상블의 이득 중 상당 부분은 모델들이 서로 다른 실수를 하는 데서 오는데, 가중치를 나눠 쓰는 모델들은 실수도 닮기 때문이다.
그래서 "드롭아웃은 앙상블이다"는 계산의 성질을 설명하는 비유이지 같은 것이라는 선언이 아니다. 실제로 여러 모델을 따로 학습해 평균 내는 진짜 앙상블은 드롭아웃 위에 얹어도 여전히 성능을 올려 준다. 두 기법이 겹치지 않는 부분이 남아 있다는 뜻이다.
변형들
Dropout2d
합성곱 신경망에 그냥 드롭아웃을 걸면 기대만큼 듣지 않는다. 특성 맵에서 이웃한 픽셀들은 거의 같은 값을 갖기 때문이다. 한 픽셀을 꺼도 옆 픽셀이 같은 정보를 그대로 들고 있으니 끊긴 것이 없고, 다음 층은 꺼진 자리를 어렵지 않게 메운다. 공간적으로 값이 붙어 있다는 성질이 노이즈를 흡수해 버리는 것이다.
그래서 채널 단위로 끈다. nn.Dropout2d는 텐서에서 채널을 골라 그 채널의 전부를 0으로 만든다. 채널 하나가 대략 "특정 무늬를 찾는 필터의 응답"에 해당하므로, 이것은 그 무늬를 이번 스텝에 못 쓰게 막는 일이 된다. 픽셀을 끄는 것과 달리 다른 채널이 대신해 주기 어렵다.
DropPath
더 굵은 단위로 끄는 변형도 있다. DropPath는 잔차 블록 하나를 통째로 건너뛴다. 잔차 연결이 꼴이므로 를 0으로 만들면 입력이 그대로 통과하고, 그 블록은 이번 샘플에 대해 없는 것이 된다. 배치 전체가 아니라 샘플마다 따로 정하므로 같은 미니배치 안에서도 샘플마다 실제 깊이가 달라진다 — 그래서 Stochastic Depth라고도 부른다.
ViT나 ConvNeXt 같은 모델은 이 확률을 블록 깊이에 따라 선형으로 키운다. 첫 블록은 0에 가깝고 마지막 블록이 설정한 최대값을 받는다. 앞쪽 블록은 뒤쪽 전부가 기대는 기본 특징을 만들어 함부로 끊으면 손해가 크고, 뒤쪽 블록은 그 위에 얹는 세부라 건너뛰어도 덜 아프다는 판단이다.
# Dropout2d: CNN에서 채널 단위로 비활성화
# (공간 전체 채널을 끔 — 특성 맵 단위 정규화)
conv_dropout = nn.Dropout2d(p=0.2)
feat_map = torch.randn(8, 64, 16, 16) # (B, C, H, W)
out = conv_dropout(feat_map)
# DropPath (Stochastic Depth): 잔차 경로 단위로 끔
# — 샘플별로 전체 블록을 스킵 (ViT, DeiT에서 사용)
import torch
def drop_path(x, drop_prob, training):
if not training or drop_prob == 0:
return x
keep = 1 - drop_prob
shape = (x.shape[0],) + (1,) * (x.ndim - 1)
mask = x.new_empty(shape).bernoulli_(keep) / keep
return x * mask
어텐션 드롭아웃
트랜스포머 블록에는 드롭아웃이 세 자리에 들어간다. 어텐션 확률 행렬, 피드포워드 층의 중간 활성값, 그리고 각 부분층의 출력이 잔차에 더해지기 직전이다. 이 중 첫 번째가 특이하다 — 어텐션 확률을 끄는 것은 뉴런이 아니라 토큰 쌍의 연결을 끊는 일이기 때문이다.
어텐션 확률에 거는 드롭아웃은 하나 더 특이한 점이 있다. 확률 행렬은 행마다 합이 1이 되도록 정규화된 뒤에 드롭아웃을 지나므로, 몇 자리가 꺼지고 나머지가 스케일되면 그 합이 더는 1이 아니다. 가중 평균이라는 성질이 훈련 중에는 깨지는 셈인데, 이 정도의 흐트러짐은 그대로 두는 것이 관례다.
이렇게 보면 공적응 이야기가 한 층 위에서 되풀이된다. 어떤 토큰이 언제나 특정 토큰 하나만 보도록 굳는 것을 막고, 여러 자리에서 근거를 모으게 만드는 것이다. drop_path가 블록 단위, Dropout2d가 채널 단위인 것과 마찬가지로, 무엇을 끄느냐는 그 구조에서 무엇이 서로 기댈 수 있는 단위인가로 정해진다.
MC Dropout
여러 번 순전파
지금까지는 추론에서 드롭아웃을 끄는 것이 규칙이었다. 일부러 켜 두면 새로운 쓸모가 생긴다. 같은 입력을 켠 채로 여러 번 통과시키면 매번 다른 서브네트워크가 답하므로 예측이 조금씩 달라지는데, 이 값들의 평균을 예측으로 쓰고 퍼진 정도를 모델이 얼마나 헷갈리는지의 척도로 쓰는 것이다. 이 방법을 MC Dropout(Monte Carlo Dropout)이라 한다.
def mc_dropout_predict(model, x, n_samples=50):
model.train() # 드롭아웃 활성화 (추론 시에도)
preds = []
with torch.no_grad():
for _ in range(n_samples):
preds.append(torch.softmax(model(x), dim=-1))
preds = torch.stack(preds) # (n_samples, B, C)
mean = preds.mean(0) # 평균 예측
std = preds.std(0) # 불확실성 (표준편차)
return mean, std
# 불확실성이 높은 샘플 탐지
mean_pred, uncertainty = mc_dropout_predict(model, X_test[:10])
print(f"예측 불확실성: {uncertainty.max(1).values}")
# 높은 값 → 모델이 확신하지 못하는 샘플
베이지안 근사의 범위
MC Dropout이 자주 "베이지안 추론의 근사"라고 소개되는 것은 Gal과 Ghahramani가 2016년에 드롭아웃 훈련이 특정 조건 아래 가우시안 과정의 변분 근사와 같아진다는 것을 보였기 때문이다. 근거 있는 주장이지만 그 동치가 성립하는 조건이 있고, 실무에서는 세 가지를 기억해 두는 편이 낫다.
첫째, 이 방법이 재는 것은 에피스테믹 불확실성이다. 모델이 그 영역의 데이터를 충분히 못 봐서 생기는 불확실성을 뜻하고, 데이터 자체에 섞인 잡음에서 오는 불확실성은 여기에 잡히지 않는다. 라벨이 원래 애매한 샘플은 모델이 자신 있게 틀리므로 이 지표가 낮게 나온다. 둘째, 퍼진 정도의 절대값은 와 표본 수에 따라 달라지므로 그 값을 곧바로 확률로 읽으면 안 된다. 셋째, 그래서 이 값은 순위를 매기는 데 쓰는 것이 안전하다 — 어떤 입력이 더 위험한지를 가려 사람에게 넘기거나 재검토 큐에 넣는 용도다.
표본 수와 모드
코드에서 model.train()을 부른 것은 드롭아웃을 켜기 위해서지만, 이 한 줄은 배치 정규화까지 훈련 모드로 만든다. 그러면 추론 중에 실행 통계가 갱신되어 모델이 조용히 변한다. 드롭아웃 모듈만 골라 켜는 편이 안전하다 — for m in model.modules(): if isinstance(m, nn.Dropout): m.train() 꼴이다.
표본 수는 보통 20에서 50 사이를 쓴다. 적으면 퍼진 정도의 추정이 불안정하고, 늘릴수록 좋아지지만 개선 폭이 금세 완만해진다. 비용은 정직하게 표본 수에 비례해서 50번이면 추론이 50배 걸린다. 그래서 모든 요청에 거는 것이 아니라 결정이 중요한 구간에만 선택적으로 거는 구성이 현실적이다.
p 고르기
자리별 값
드롭아웃을 어디에 얼마나 걸지는 그 층에 파라미터가 얼마나 몰려 있고 그 값들이 서로 얼마나 겹치는지로 정해진다. 완전 연결 층은 파라미터가 많고 뉴런끼리 겹침이 적어 높은 값을 견디지만, 합성곱 층은 앞서 본 공간 상관 때문에 같은 값을 걸면 학습이 흔들린다.
| 적용 위치 | 권장 p | 이유 |
|---|---|---|
| FC 은닉층 (MLP) | 0.4~0.5 | 과적합 강하게 방지 |
| CNN 마지막 FC | 0.3~0.5 | 과적합 방지 |
| CNN 컨볼루션 후 | 0.1~0.2 | 낮게 (공간 의존성) |
| 트랜스포머 FFN | 0.1~0.2 | 너무 크면 성능 저하 |
| DropPath (ViT) | 0.0~0.2 | 모델 깊이에 따라 |
| 출력층 | 0 (적용 안 함) | 마지막 예측에 노이즈 금지 |
마지막 줄이 예외 없는 규칙이다. 출력층에 드롭아웃을 걸면 분류 로짓 일부가 무작위로 0이 되어 그 클래스의 점수가 이유 없이 내려간다. 정규화가 아니라 그냥 답을 망가뜨리는 일이다.
표의 값은 출발점이지 정답이 아니다. 고르는 순서를 정해 두면 헤매지 않는다 — 먼저 드롭아웃 없이 학습해 훈련 손실과 검증 손실이 벌어지는지를 본다. 두 곡선이 붙어 있으면 과적합이 아직 문제가 아니므로 드롭아웃을 넣을 이유가 없고, 벌어지기 시작하면 그 폭을 보고 값을 올린다. 반대로 값을 올렸는데 훈련 손실까지 함께 나빠졌다면 정규화가 과해 모델이 데이터를 담을 여력을 잃은 것이니 되돌린다. 검증 손실 하나만 보고 값을 움직이면 두 경우가 구별되지 않는다.
import torch.nn as nn
class MLPWithDropout(nn.Module):
def __init__(self, in_dim, hidden_dims, out_dim,
dropout_p=0.5):
super().__init__()
layers = []
prev_dim = in_dim
for h_dim in hidden_dims:
layers.extend([
nn.Linear(prev_dim, h_dim),
nn.BatchNorm1d(h_dim),
nn.ReLU(),
nn.Dropout(p=dropout_p),
])
prev_dim = h_dim
# 출력층에는 드롭아웃 적용하지 않음
layers.append(nn.Linear(prev_dim, out_dim))
self.net = nn.Sequential(*layers)
def forward(self, x):
return self.net(x)
model = MLPWithDropout(784, [512, 256, 128], 10, dropout_p=0.4)
배치 정규화와 함께
둘을 같이 쓸 때 생기는 문제가 하나 있다. 드롭아웃은 훈련에서 활성값의 분산을 키우는데, 배치 정규화는 훈련 중에 본 통계를 실행 평균으로 기억해 두었다가 추론에서 그대로 쓴다. 추론에서 드롭아웃이 꺼지면 실제 분산이 기억해 둔 값과 달라져, 정규화가 잘못된 기준으로 이뤄진다. 이 어긋남을 분산 불일치(variance shift)라 한다.
피하는 방법은 배치 정규화가 드롭아웃의 영향을 받지 않는 자리에 오게 두는 것이다. 위 코드처럼 BatchNorm → ReLU → Dropout 순서로 쌓으면 정규화가 드롭아웃보다 앞에 있으므로 통계가 오염되지 않는다. 합성곱 신경망에서 드롭아웃을 아예 빼고 배치 정규화만 쓰는 구성이 표준이 된 것도 이 문제 때문이고, 그 경우 정규화 역할은 데이터 증강과 가중치 감쇠가 나눠 맡는다.
큰 모델의 낮은 p
요즘의 큰 모델들은 를 낮게 잡거나 0으로 둔다. 이유는 두 가지다. 하나는 데이터가 모델보다 크면 외울 여지 자체가 줄어 과적합이 주된 위험이 아니게 된다는 것이고, 다른 하나는 드롭아웃이 매 스텝 잡음을 넣어 수렴을 늦춘다는 것이다. 전체 데이터를 한 바퀴도 다 못 도는 대규모 사전학습에서는 이득은 안 오고 비용만 남는다.
그렇다고 드롭아웃이 낡은 기법이 된 것은 아니다. 같은 모델을 작은 데이터로 파인튜닝하는 순간 조건이 정반대가 되어 과적합이 다시 주된 위험이 되고, 그때는 값을 올려 잡는다. 결국 는 모델 크기가 아니라 모델과 데이터의 비율이 정하는 값이다. 지금 하는 일이 그 비율의 어느 쪽에 있는지를 보고 고르면 된다.
읽어주셔서 감사합니다. 😊

