Module 7

데이터 드리프트 & 재학습

MLOps 과정 | 배포 이후의 모델 관리

Section 1

드리프트란?

데이터 드리프트 감지 & 재학습 판단

배포 이후, 모델 성능은 왜 떨어지는가?

드리프트(Drift) 개념

모델은 학습 시점의 데이터 분포를 기반으로 판단합니다.
시간이 지나면서 입력 데이터의 분포가 변하면 → 모델 성능이 하락합니다.

예시: 대출 심사 모델
2024년에 학습한 모델인데, 2025년 금리 급등으로
연소득/대출신청액 분포가 크게 변화 → 예측 정확도 급락

경기 침체 시나리오

변화설명
연소득 하락 / 근속연수 단축이직, 해고 증가
부채비율 상승 / 신용점수 하락경제 악화 영향
대출신청액 증가급전 수요 증가
승인률 급락61% → 8%

모델은 바뀌지 않았는데, 입력 데이터가 바뀌면서 결과가 완전히 달라짐

Section 2

PSI로 드리프트 감지

PSI (Population Stability Index)

한국 금융권(NICE, KCB)에서 실제 사용하는 표준 드리프트 지표

PSI 값판정의미
< 0.1안정분포 변화 없음
0.1 ~ 0.25주의약간의 변화 감지
> 0.25드리프트분포가 크게 변함 → 재학습 필요

PSI 계산 코드

import numpy as np

def calculate_psi(baseline, current, bins=10):
    """PSI (Population Stability Index) 계산"""
    # 기준 데이터로 구간(bin) 경계 생성
    breakpoints = np.percentile(baseline, np.linspace(0, 100, bins + 1))

    # 각 구간별 비율 계산
    baseline_counts = np.histogram(baseline, bins=breakpoints)[0]
    current_counts = np.histogram(current, bins=breakpoints)[0]

    # 비율로 변환 (0 방지)
    baseline_pct = (baseline_counts + 1) / (len(baseline) + bins)
    current_pct = (current_counts + 1) / (len(current) + bins)

    # PSI 공식: Σ (P_i - Q_i) × ln(P_i / Q_i)
    psi = np.sum((current_pct - baseline_pct) * np.log(current_pct / baseline_pct))
    return psi

피처별 드리프트 리포트

========== 드리프트 리포트 ==========

Feature               PSI       Status
─────────────────────────────────────
annual_income         0.4821    ⚠ DRIFT
loan_amount           0.3217    ⚠ DRIFT
debt_to_income        0.2894    ⚠ DRIFT
credit_score          0.1932    ⚡ WARNING
employment_years      0.1547    ⚡ WARNING
interest_rate         0.0823    ✅ STABLE
─────────────────────────────────────

드리프트 감지: 3개 피처 (PSI > 0.25)
주의 필요: 2개 피처 (PSI 0.1~0.25)
안정: 1개 피처 (PSI < 0.1)

PSI 시각화

Bar Chart로 피처별 PSI 비교

각 피처별 PSI 값을 막대 그래프로 표시하고,
기준선 2개를 추가합니다:

- 빨간 점선 (PSI = 0.25): 드리프트 기준선
- 주황 점선 (PSI = 0.1): 주의 기준선

빨간 점선 위의 막대 = 재학습 필요 피처
plt.axhline(y=0.25, color='red', linestyle='--', label='Drift')
plt.axhline(y=0.1, color='orange', linestyle='--', label='Warning')

Section 3

예측 로그 활용

운영 데이터는 어디서 오는가?

실무에서는 별도 CSV 파일이 아니라 예측 로그에서 수집합니다.

API 요청
입력 데이터
로그 기록
일정 기간
쌓이면
학습 데이터와
분포 비교

예측할 때마다 입력값을 로그로 남기면 → 운영 데이터가 자동으로 쌓임

CloudWatch → 드리프트 감지

방법구성규모
방법 1 Logs Insights 수동 추출 소규모
방법 2 S3 자동 내보내기 + 배치잡 가장 흔함
방법 3 Kinesis + Lambda 실시간 대규모 실시간

방법 2: S3 자동 내보내기

CloudWatch
Logs
S3 자동
내보내기
일 1회
Python/Pandas
PSI 계산
드리프트 감지 시
Slack 알림

가장 흔한 방식: 매일 자동으로 로그를 S3에 저장하고,
배치 스크립트가 PSI를 계산해서 드리프트 여부를 판단

Section 4

재학습 판단 & 실행

재학습 판단 흐름

PSI 계산
0.25 초과?
알림 전송
재학습 트리거

# 드리프트 판단 로직
psi = calculate_psi(baseline_data, current_data)

if psi > 0.25:
    print("⚠ 드리프트 감지! 재학습 필요")
    send_slack_alert(f"PSI = {psi:.4f} → 재학습 트리거")
    trigger_retraining()
elif psi > 0.1:
    print("⚡ 주의: 분포 변화 감지")
else:
    print("✅ 안정: 분포 변화 없음")

재학습 후 성능 비교

새 모델이 더 좋을 때만 배포합니다. (기존 모델 vs 새 모델 AUC 비교)

# 기존 모델 vs 새 모델 성능 비교
old_auc = evaluate_model(old_model, test_data)  # 예: 0.72
new_auc = evaluate_model(new_model, test_data)  # 예: 0.89

print(f"기존 모델 AUC: {old_auc:.4f}")
print(f"새 모델 AUC:   {new_auc:.4f}")

if new_auc > old_auc:
    print("✅ 새 모델이 더 좋음 → 배포 진행")
    deploy_model(new_model)
else:
    print("⚠ 기존 모델 유지 (새 모델 성능 부족)")

모델 배포 방식 비교

우리 과정실무
모델 저장 Git에 pkl 파일 포함 S3에 업로드
모델 포함 Docker 이미지에 포함 시작 시 S3에서 다운로드
모델 교체 git push → CI/CD 전체 실행 ECS 재시작만으로 교체

실무에서는 모델과 코드를 분리합니다.
모델만 바꾸는데 Docker 빌드를 다시 할 필요 없음!

Section 5

실무 S3 모델 배포

S3 모델 로딩 코드

우리 과정 (로컬 로드)

import joblib

model = joblib.load(
    'models/loan_pipeline.pkl'
)

실무 (S3 로드)

import boto3
import joblib

s3 = boto3.client('s3')

s3.download_file(
    'my-model-bucket',
    'models/loan_pipeline.pkl',
    '/tmp/loan_pipeline.pkl'
)

model = joblib.load(
    '/tmp/loan_pipeline.pkl'
)

lifespan에서 한 번만 로드

from contextlib import asynccontextmanager
import boto3, joblib

@asynccontextmanager
async def lifespan(app):
    # 시작 시 S3에서 모델 다운로드
    s3 = boto3.client('s3')
    s3.download_file('my-bucket', 'models/latest.pkl', '/tmp/model.pkl')
    app.state.model = joblib.load('/tmp/model.pkl')
    print("✅ S3에서 모델 로드 완료")
    yield

app = FastAPI(lifespan=lifespan)

모델을 바꾸려면 ECS 재시작만 하면 됩니다.
재시작하면 lifespan이 다시 실행 → S3에서 최신 모델 가져옴

S3 → Lambda → ECS 자동 재배포

S3에 새 모델
업로드
S3 이벤트
트리거
Lambda
함수 실행
ECS 강제
재배포

새 태스크
시작
S3에서 최신
모델 다운로드
서비스
자동 교체 완료

S3에 모델만 올리면 → 자동으로 최신 모델로 서비스가 교체됩니다.

Section 6

Airflow

Airflow란?

작업 스케줄러
"이 작업을 언제, 어떤 순서로 실행해라"를 코드로 정의합니다.

윈도우 작업 스케줄러 / crontab의 고급 버전

- 작업 간 의존성 정의 가능 (A 끝나면 B 실행)
- 실패 시 재시도, 알림 자동화
- 웹 UI에서 실행 상태 모니터링

Airflow DAG 코드 예시

from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime

dag = DAG(
    'drift_detection',
    schedule_interval='@daily',        # 매일 실행
    start_date=datetime(2025, 1, 1),
)

t1 = PythonOperator(task_id='fetch_logs',       python_callable=fetch_logs,       dag=dag)
t2 = PythonOperator(task_id='calculate_drift',  python_callable=calculate_drift,  dag=dag)
t3 = PythonOperator(task_id='check_and_alert',  python_callable=check_and_alert,  dag=dag)
t4 = PythonOperator(task_id='retrain',          python_callable=retrain_model,    dag=dag)

t1 >> t2 >> t3 >> t4

t1 >> t2 >> t3 >> t4

Python 연산자 오버로딩

>>는 원래 비트 시프트 연산자인데,
Airflow가 __rshift__를 재정의해서
"다음 작업" 의미로 사용합니다.

# 아래 두 줄은 동일한 의미
t1 >> t2 >> t3 >> t4

t1.set_downstream(t2)
t2.set_downstream(t3)
t3.set_downstream(t4)

fetch_logs
calculate_drift
check_and_alert
retrain

우리 실습 vs Airflow

우리 실습Airflow
데이터 수집 prediction_logs.csv 수동 로드 자동으로 S3에서 가져옴
드리프트 계산 수동 PSI 계산 자동 배치잡
판단 눈으로 판단 자동 알림 + 트리거

실습에서 수동으로 한 모든 것을 → Airflow가 자동화합니다.

Section 7

전체 정리

MLOps 운영 자동화 전체 사이클

코드 Push
CI/CD
배포
예측 로그
수집
재배포
재학습
드리프트
감지
예측 로그
수집

배포 이후에도 지속적으로 모니터링 → 감지 → 재학습 → 재배포하는
순환 구조가 MLOps의 핵심입니다.

Module 7 정리

  • 드리프트 = 입력 데이터 분포 변화
  • PSI = 금융권 표준 드리프트 지표
  • 예측 로그에서 운영 데이터 수집
  • 재학습 후 성능 비교 → 더 좋을 때만 배포
  • 실무에서는 S3 + ECS 재시작으로 모델 교체
  • Airflow로 전체 흐름 자동화

코드 Push → CI/CD → 배포 → 로그 수집 → 드리프트 감지 → 재학습 → 재배포
이 전체 사이클이 MLOps입니다.

Section 8

재학습 자동화 파이프라인

재학습 자동화 파이프라인

PSI 드리프트
감지 (자동)
Slack 알림
(자동)
사람이 판단
"재학습하자"
retrain.py
실행
S3 업로드
(자동)
Lambda
트리거 (자동)
ECS 재시작
(자동)
새 모델로
서빙 시작

핵심: 사람은 "재학습 결정"만 한다. 나머지는 전부 자동.
python scripts/retrain.py 한 줄이면 학습 → 비교 → 저장 → 배포까지 완료.

retrain.py 구조

파일 하나로 재학습 파이프라인 전체를 실행

단계함수설명
[1/4]load_data()학습 데이터 + 예측 로그 로드 & 결합
[2/4]train_model()기존 모델 vs 새 모델 학습 & AUC 비교
[3/4]배포 판정AUC >= 0.90 && 개선폭 >= 0.0 → 배포
[4/4]save_model()
upload_to_s3()
로컬 저장 + S3 업로드 (→ Lambda → ECS)

주요 인자: --dry-run (테스트만), --force (강제 배포), --bucket (S3 버킷 지정), --cluster / --service (ECS 설정)

S3 + Lambda = 자동 배포

retrain.py
S3 업로드
S3 이벤트
(PUT .pkl)
Lambda 함수
자동 실행
ECS
force-new-deployment

# Lambda 함수 (model-update-trigger)
import boto3

def lambda_handler(event, context):
    ecs = boto3.client('ecs')
    ecs.update_service(
        cluster='mlops-cluster',
        service='loan-api-service',
        forceNewDeployment=True,
    )
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = event['Records'][0]['s3']['object']['key']
    print(f'모델 업데이트: s3://{bucket}/{key}')
    return {'statusCode': 200}

S3 이벤트 설정: Event type = PUT, Prefix = loan-api/, Suffix = .pkl

model.py 수정 — S3에서 모델 로드

환경변수 패턴: MODEL_S3_BUCKET이 설정되면 S3에서 다운로드, 없으면 로컬 로드

def load(self, model_dir='models'):
    s3_bucket = os.environ.get('MODEL_S3_BUCKET')
    s3_prefix = os.environ.get('MODEL_S3_PREFIX', 'loan-api')

    if s3_bucket:
        # S3에서 모델 다운로드
        s3 = boto3.client('s3')
        for fname in ['loan_pipeline.pkl',
                      'label_encoders.pkl',
                      'feature_names.pkl']:
            s3.download_file(s3_bucket,
                f'{s3_prefix}/{fname}',
                os.path.join(model_dir, fname))
            logger.info(f'S3 다운로드: {fname}')

    # 로컬에서 로드 (S3 다운로드 후 or 기존 로컬 파일)
    self.pipeline = joblib.load(f'{model_dir}/loan_pipeline.pkl')
    self.label_encoders = joblib.load(f'{model_dir}/label_encoders.pkl')

ECS Task Definition 환경변수:
MODEL_S3_BUCKET=mlops-lab-models-{ACCOUNT_ID}
MODEL_S3_PREFIX=loan-api

현업 자동화 수준

Level방식적합한 조직도구
Level 0 수동 (ssh → 파일 교체) 초기 프로토타입 수작업
Level 1 retrain.py + 수동/cron 스타트업 Python 스크립트
Level 2 S3 이벤트 → Lambda → ECS 중소기업 AWS Lambda
Level 3 Airflow/Step Functions 중견~대기업 Airflow, SageMaker

모든 수준의 공통점: 재학습 "결정"은 사람이 한다.
완전 자동 재학습은 위험하다 — 잘못된 데이터로 학습하면 장애 발생.
Human-in-the-loop = 현업의 표준.

데모: retrain.py 실행

$ python scripts/retrain.py

2026-03-24 16:30:00 [INFO] ============================================================
2026-03-24 16:30:00 [INFO]   재학습 파이프라인 시작 (버전: v20260324_163000)
2026-03-24 16:30:00 [INFO] ============================================================
2026-03-24 16:30:00 [INFO] [Slack 알림] 재학습 파이프라인 시작

2026-03-24 16:30:00 [INFO] [1/4] 데이터 로드
2026-03-24 16:30:00 [INFO] 학습 데이터: 10000건 (승인율 61.3%)
2026-03-24 16:30:00 [INFO] 예측 로그:   2847건  (승인율 8.2%)
2026-03-24 16:30:00 [INFO] 결합 데이터: 12847건

2026-03-24 16:30:02 [INFO] [2/4] 모델 학습 & 성능 비교
2026-03-24 16:30:02 [INFO]   기존 모델: AUC = 0.9234
2026-03-24 16:30:03 [INFO]   새 모델:   AUC = 0.9312 (+0.0078)

2026-03-24 16:30:03 [INFO]   Feature Importance Top 5:
2026-03-24 16:30:03 [INFO]     1. 신용점수: 0.2841
2026-03-24 16:30:03 [INFO]     2. 연소득: 0.1923
2026-03-24 16:30:03 [INFO]     3. 부채비율: 0.1547

2026-03-24 16:30:03 [INFO] [3/4] 배포 판정
2026-03-24 16:30:03 [INFO]   PASS: 배포 기준 충족

2026-03-24 16:30:03 [INFO] [4/4] 모델 저장 & 배포
2026-03-24 16:30:04 [INFO]   S3 업로드 완료!
2026-03-24 16:30:04 [INFO]   -> Lambda -> ECS 자동 재시작
2026-03-24 16:30:04 [INFO] [Slack 알림] 새 모델 배포 완료!