본문 바로가기
푸닥거리

Label Shift vs Covariate Shift — 실무 개발자를 위한 10분 가이드

by ┌(  ̄∇ ̄)┘™ 2025. 8. 23.
728x90

모델을 배포하면 “학습 때 잘되던 성능이 왜 떨어졌지?” 같은 일이 종종 생깁니다. 대부분은 데이터 분포 이동(distribution shift) 때문이죠. 이 글에서는 특히 자주 등장하는 두 가지: Covariate shift(공변량 이동)와 Label shift(레이블 이동)을 비교·정리하고, 실무에 쓸 수 있는 진단/대응 코드를 함께 제공합니다.


요약표

구분Covariate shift (공변량 이동)Label shift (레이블 이동)
바뀌는 분포 입력 P(x)P(x) 레이블 P(y)P(y)
같다고 가정 P(y∣x)P(y\mid x) P(x∣y)P(x\mid y)
수식 Ptr(x)≠Pte(x)P_{tr}(x)\neq P_{te}(x), but Ptr(y∣x)=Pte(y∣x)P_{tr}(y\mid x)=P_{te}(y\mid x) Ptr(y)≠Pte(y)P_{tr}(y)\neq P_{te}(y), but Ptr(x∣y)=Pte(x∣y)P_{tr}(x\mid y)=P_{te}(x\mid y)
직관 환경/특징 분포가 달라짐 클래스 비율이 달라짐
예시 낮-맑음으로 학습 → 밤-비 환경에 배포 학습: 환자 50%/정상 50% ↔ 실제: 환자 5%/정상 95%
주된 대응 중요도 가중치(Importance weighting), 도메인 적응 사전확률 보정(Prior correction), 재가중(Reweighting)

참고: Concept driftP(y∣x)P(y\mid x) 자체가 변하는 경우(규칙/정책 변경 등)로 위 두 가지보다 더 어렵습니다.


1) Covariate Shift 자세히 보기

정의

  • 학습과 배포에서 입력 분포 P(x)P(x) 가 달라지지만, 조건부 분포 P(y∣x)P(y\mid x) 는 같다고 가정.
  • 예) 카메라/센서 교체, 조명·날씨·언어 차이, 수집 채널 변경 등.

왜 성능이 떨어질까?

  • 모델이 보지 못했던 입력 영역의 샘플이 늘어나면 일반화가 깨집니다.

어떻게 진단하나?

  • 특징별 히스토그램/KS 검정, 임베딩 후 MMD 등 분포 비교
  • 도메인 분류기: train vs test를 분리하는 분류기를 학습해 AUC가 높으면 분포 차이가 큼

무엇을 할까?

  • Importance weighting: w(x)=Pte(x)Ptr(x)w(x)=\frac{P_{te}(x)}{P_{tr}(x)} 로 샘플 가중
  • Domain adaptation: 표준화·증강, 특징 정렬(CORAL), 도메인 불변 표현(DANN) 등
  • 데이터 전략: 실제 배포 도메인에서 데이터 추가 수집

실무 미니 코드: 도메인 분류기 기반 가중치 (scikit-learn)

 
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier

# X_tr, y_tr: 학습 데이터/레이블
# X_te: 배포(또는 최근) 입력 샘플(레이블 없음 가정)

# 1) train vs test 도메인 분류기 학습
X_dom = np.vstack([X_tr, X_te])
y_dom = np.hstack([np.zeros(len(X_tr)), np.ones(len(X_te))])  # 0=train, 1=test
dom_clf = LogisticRegression(max_iter=1000).fit(X_dom, y_dom)

# 2) p(test|x)를 이용해 중요도 가중치 근사
p_te = dom_clf.predict_proba(X_tr)[:, 1]  # on train points
# 비율 보정(선택): n_tr/n_te 곱하기
ratio = len(X_tr) / len(X_te)
w = (p_te / (1 - p_te)) * ratio
w = np.clip(w, 1e-3, np.percentile(w, 99))  # 안정화

# 3) 가중치로 최종 모델 재학습
clf = RandomForestClassifier()
clf.fit(X_tr, y_tr, sample_weight=w)

2) Label Shift 자세히 보기

정의

  • 학습과 배포에서 레이블 분포 P(y)P(y) 가 달라지지만, 클래스 내 입력 분포 P(x∣y)P(x\mid y) 는 같다고 가정.
  • 예) 시기/이벤트에 따라 특정 카테고리 발생 빈도가 급증/급감

왜 성능이 떨어질까?

  • 임계치 또는 확률 보정이 학습 당시 클래스 비율에 맞춰져 있어, 실제 클래스 비율과 어긋나면 과/과소 예측이 발생.

어떻게 진단하나?

  • 테스트 입력에 대한 예측 분포 P(y^)P(\hat y) 가 학습의 P(y)P(y)와 크게 다르면 Label shift 의심
  • BBSE/EM 방식으로 테스트 사전확률 πte(y)\pi_{te}(y) 추정
728x90

무엇을 할까?

  • Prior correction: 테스트 사전확률 πte(y)\pi_{te}(y) 를 추정해Pte(y∣x)∝Ptr(y∣x)⋅πte(y)πtr(y)P_{te}(y\mid x)\propto P_{tr}(y\mid x)\cdot \frac{\pi_{te}(y)}{\pi_{tr}(y)}로 확률 재보정(임계치 재설정)
  • Reweighting: 학습 시 클래스별 가중치를 목표 비율에 맞춤

실무 미니 코드: BBSE 스타일 간단 구현

아이디어: (1) 검증 세트에서 혼동행렬로 P(y^=i∣y=j)P(\hat y=i\mid y=j) 추정, (2) 테스트에서 P(y^=i)P(\hat y=i) 집계 → (3) πte\pi_{te} 를 선형시스템으로 풀기.

 
import numpy as np
from numpy.linalg import lstsq

# val_pred: 검증셋에서의 예측 레이블(또는 확률 argmax)
# val_true: 검증셋 정답
# te_pred: 테스트셋 예측 레이블(또는 확률 argmax)
K = len(np.unique(val_true))

# 1) C[i,j] = P(\hat y=i | y=j)
C = np.zeros((K, K))
for j in range(K):
    idx = (val_true == j)
    counts = np.bincount(val_pred[idx], minlength=K)
    C[:, j] = counts / max(1, idx.sum())

# 2) mu[i] = P_hat(\hat y=i) on test
mu = np.bincount(te_pred, minlength=K).astype(float)
mu /= mu.sum()

# 3) C * pi_te ≈ mu  -> 최소제곱
pi_te, *_ = lstsq(C, mu, rcond=None)
pi_te = np.clip(pi_te, 1e-6, 1.0)
pi_te /= pi_te.sum()  # 정규화

# 4) prior correction 계수
# 학습 사전확률 pi_tr는 학습 데이터의 클래스 비율로 계산
pi_tr = np.bincount(y_tr, minlength=K).astype(float)
pi_tr /= pi_tr.sum()
alpha = pi_te / np.clip(pi_tr, 1e-6, 1.0)

# 5) 소프트 예측 확률 p_tr(y|x)에 prior 보정 적용(예: 로지스틱 회귀/NN의 softmax 출력)
# p_tr_proba: shape (N_test, K)
def prior_correct(p_tr_proba, alpha):
    q = p_tr_proba * alpha  # 브로드캐스트
    q /= q.sum(axis=1, keepdims=True)
    return q

# 사용 예: p_te = prior_correct(p_tr_proba, alpha)
 

실무 팁

  • 보정 후에는 임계치(decision threshold)도 다시 튜닝하세요(예: PR AUC 관점에서).
  • 클래스가 많고 C가 불안정하면 라플라스 스무딩, Tikhonov 정규화 등을 고려하세요.

3) 현업 체크리스트 (바로 적용)

  1. 입력 통계 변화 탐지: 간단한 도메인 분류기 AUC / 피처 히스토그램 비교
  2. 예측 분포 모니터링: P(y^)P(\hat y) 시계열 대시보드(학습 P(y)P(y)와 차이 감시)
  3. 라벨 없는 환경 대응:
    • Covariate: 도메인 분류기 기반 중요도 가중, 간단한 표준화/증강
    • Label: BBSE/EM로 πte\pi_{te} 추정 후 prior correction
  4. 둘 다 가능: 입력 정렬(도메인 적응) + 사전확률 보정 동시 적용
  5. 배포 전 검증: 샌드박스/그레이 롤아웃에서 모니터링 메트릭(예: PSI, JS divergence, P(y^)P(\hat y)) 체크

4) 헷갈리는 포인트 정리

  • Imbalance ≠ Label shift
    학습 데이터가 불균형인 건 label shift가 아님. 핵심은 배포 시 비율이 바뀌었는가.
  • Concept drift와의 구분
    concept drift는 P(y∣x)P(y\mid x) 자체가 변하는 것. 정책 변경, 사용자 행동 급변 등.
  • 평가 데이터 선택
    최신 분포를 반영하지 못하는 검증세트는 “그럴싸한 착시 성능”을 줍니다. 시간·도메인 분할로 만든 검증세트를 권장.

5) 간단 도식(텍스트)

 
Covariate shift:
P_tr(x)  ≠  P_te(x)
P_tr(y|x)  =  P_te(y|x)
[환경/피처가 변함 → 입력을 맞추거나 가중치로 보정]

Label shift:
P_tr(y)  ≠  P_te(y)
P_tr(x|y)  =  P_te(x|y)
[클래스 비율이 변함 → 사전확률/임계치 보정]

6) 마무리

  • Covariate shift는 “환경이 변했다” 문제, Label shift는 “비율이 변했다” 문제입니다.
  • 간단한 도메인 분류기예측 분포 모니터링만으로도 조기 경보가 가능하고,
    가중치/보정으로 실전 성능 하락을 상당 부분 줄일 수 있습니다.
728x90

댓글