Module 7
데이터 드리프트 & 재학습
MLOps 과정 | 배포 이후의 모델 관리
데이터 드리프트 감지 & 재학습 판단
배포 이후, 모델 성능은 왜 떨어지는가?
드리프트(Drift) 개념
모델은 학습 시점의 데이터 분포를 기반으로 판단합니다.
시간이 지나면서 입력 데이터의 분포가 변하면 → 모델 성능이 하락합니다.
예시: 대출 심사 모델
2024년에 학습한 모델인데, 2025년 금리 급등으로
연소득/대출신청액 분포가 크게 변화 → 예측 정확도 급락
경기 침체 시나리오
| 변화 | 설명 |
| 연소득 하락 / 근속연수 단축 | 이직, 해고 증가 |
| 부채비율 상승 / 신용점수 하락 | 경제 악화 영향 |
| 대출신청액 증가 | 급전 수요 증가 |
| 승인률 급락 | 61% → 8% |
모델은 바뀌지 않았는데, 입력 데이터가 바뀌면서 결과가 완전히 달라짐
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')
운영 데이터는 어디서 오는가?
실무에서는 별도 CSV 파일이 아니라 예측 로그에서 수집합니다.
API 요청
→
입력 데이터
로그 기록
→
일정 기간
쌓이면
→
학습 데이터와
분포 비교
예측할 때마다 입력값을 로그로 남기면 → 운영 데이터가 자동으로 쌓임
CloudWatch → 드리프트 감지
| 방법 | 구성 | 규모 |
| 방법 1 |
Logs Insights 수동 추출 |
소규모 |
| 방법 2 |
S3 자동 내보내기 + 배치잡 |
가장 흔함 |
| 방법 3 |
Kinesis + Lambda 실시간 |
대규모 실시간 |
방법 2: S3 자동 내보내기
CloudWatch
Logs
→
S3 자동
내보내기
일 1회
→
Python/Pandas
PSI 계산
→
드리프트 감지 시
Slack 알림
가장 흔한 방식: 매일 자동으로 로그를 S3에 저장하고,
배치 스크립트가 PSI를 계산해서 드리프트 여부를 판단
재학습 판단 흐름
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 빌드를 다시 할 필요 없음!
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에 모델만 올리면 → 자동으로 최신 모델로 서비스가 교체됩니다.
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가 자동화합니다.
MLOps 운영 자동화 전체 사이클
코드 Push
→
CI/CD
→
배포
→
예측 로그
수집
재배포
←
재학습
←
드리프트
감지
←
예측 로그
수집
배포 이후에도 지속적으로 모니터링 → 감지 → 재학습 → 재배포하는
순환 구조가 MLOps의 핵심입니다.
Module 7 정리
- 드리프트 = 입력 데이터 분포 변화
- PSI = 금융권 표준 드리프트 지표
- 예측 로그에서 운영 데이터 수집
- 재학습 후 성능 비교 → 더 좋을 때만 배포
- 실무에서는 S3 + ECS 재시작으로 모델 교체
- Airflow로 전체 흐름 자동화
코드 Push → CI/CD → 배포 → 로그 수집 → 드리프트 감지 → 재학습 → 재배포
이 전체 사이클이 MLOps입니다.
재학습 자동화 파이프라인
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 알림] 새 모델 배포 완료!