🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

A

AI Generated

2025. 12. 6. · 69 Views

신용카드 사기 탐지 완벽 가이드

머신러닝을 활용한 신용카드 사기 탐지 시스템 구축 방법을 다룹니다. 극심한 클래스 불균형 문제부터 Isolation Forest, 임계값 조정까지 실무에서 바로 적용할 수 있는 핵심 기법을 배워봅니다.


목차

  1. 사기_탐지_데이터_특성
  2. 극심한_클래스_불균형_처리
  3. 언더샘플링과_오버샘플링
  4. Isolation_Forest_적용
  5. 임계값_조정_전략
  6. 정밀도_재현율_트레이드오프

1. 사기 탐지 데이터 특성

김개발 씨는 핀테크 스타트업에 입사한 지 한 달이 된 주니어 데이터 사이언티스트입니다. 어느 날 팀장님이 다가와 말했습니다.

"김개발 씨, 이번에 신용카드 사기 탐지 모델을 만들어 봐요. 데이터는 준비해 뒀으니까요." 김개발 씨는 자신 있게 데이터를 열어보았는데, 뭔가 이상했습니다.

사기 탐지 데이터는 일반적인 분류 문제와는 완전히 다른 특성을 가지고 있습니다. 마치 건초더미에서 바늘을 찾는 것처럼, 수백만 건의 정상 거래 속에서 극소수의 사기 거래를 찾아내야 합니다.

이런 데이터의 특성을 제대로 이해해야만 효과적인 모델을 구축할 수 있습니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np

# 신용카드 거래 데이터 로드
df = pd.read_csv('creditcard.csv')

# 데이터 기본 정보 확인
print(f"전체 거래 건수: {len(df):,}")
print(f"피처 개수: {df.shape[1]}")

# 클래스 분포 확인 (0: 정상, 1: 사기)
class_counts = df['Class'].value_counts()
print(f"\n정상 거래: {class_counts[0]:,} ({class_counts[0]/len(df)*100:.3f}%)")
print(f"사기 거래: {class_counts[1]:,} ({class_counts[1]/len(df)*100:.3f}%)")

# 사기 비율 계산
fraud_ratio = class_counts[1] / class_counts[0]
print(f"\n사기/정상 비율: 1:{int(1/fraud_ratio)}")

김개발 씨는 데이터를 열어보고 깜짝 놀랐습니다. 28만 건이 넘는 거래 데이터 중에서 사기 거래는 고작 492건에 불과했습니다.

비율로 따지면 전체의 0.17%도 채 되지 않았습니다. "이게 정상인가요?" 김개발 씨가 옆자리 선배 박시니어 씨에게 물었습니다.

박시니어 씨는 웃으며 대답했습니다. "당연하죠.

실제로 사기 거래가 그렇게 많으면 신용카드 회사는 벌써 망했을 거예요." 그렇습니다. 신용카드 사기 탐지 데이터의 가장 큰 특징은 바로 이 극심한 클래스 불균형입니다.

정상 거래가 압도적으로 많고, 사기 거래는 극소수에 불과합니다. 쉽게 비유하자면, 이것은 마치 학교 운동장에서 특정 학생 한 명을 찾는 것과 같습니다.

1000명의 학생 중에서 단 1명만이 빨간 모자를 쓰고 있다고 생각해 보세요. 대부분의 학생을 훑어봐도 빨간 모자는 보이지 않습니다.

이런 데이터에서 또 하나 주목해야 할 점은 피처의 익명성입니다. 보안상의 이유로 실제 신용카드 데이터는 원본 피처가 아닌 PCA 변환된 피처를 사용합니다.

V1, V2, V3처럼 의미를 알 수 없는 변수들이 28개나 됩니다. 김개발 씨는 데이터를 더 자세히 살펴보았습니다.

Time 컬럼은 첫 번째 거래로부터 경과한 시간을, Amount 컬럼은 거래 금액을 나타내고 있었습니다. 그리고 Class 컬럼이 바로 우리가 예측해야 할 타겟 변수였습니다.

"그런데 선배, 이렇게 불균형한 데이터로 모델을 학습시키면 어떻게 되나요?" 김개발 씨의 질문에 박시니어 씨가 고개를 저었습니다. "그냥 학습시키면 큰일 나요.

모델이 무조건 정상이라고만 예측해 버려도 정확도가 99.8%가 나오거든요." 이것이 바로 사기 탐지 문제의 핵심 어려움입니다. 단순히 정확도만으로는 모델의 성능을 평가할 수 없습니다.

정상 거래를 맞히는 것보다 사기 거래를 제대로 찾아내는 것이 훨씬 중요하기 때문입니다. 실제 현업에서 사기 탐지 시스템이 사기를 놓치면 어떻게 될까요?

고객은 금전적 피해를 입고, 회사는 신뢰를 잃습니다. 반면 정상 거래를 사기로 잘못 판단하면 고객 불편이 생기지만, 그래도 금전적 손실보다는 낫습니다.

이처럼 사기 탐지 데이터는 비대칭적인 비용 구조를 가지고 있습니다. 사기를 놓치는 비용이 정상을 차단하는 비용보다 훨씬 큽니다.

이 점을 항상 염두에 두어야 합니다. 김개발 씨는 고개를 끄덕였습니다.

데이터의 특성을 이해하는 것이 모델 구축의 첫걸음이라는 것을 깨달았습니다. 이제 본격적으로 이 어려운 문제를 어떻게 풀어나갈지 고민해야 할 차례였습니다.

실전 팁

💡 - 사기 탐지 데이터는 불균형이 심하므로 정확도 대신 재현율, 정밀도, F1 점수를 평가 지표로 사용하세요

  • PCA 변환된 피처는 스케일링이 이미 되어 있으므로 추가 스케일링이 필요 없습니다

2. 극심한 클래스 불균형 처리

김개발 씨는 일단 해보자는 마음으로 랜덤 포레스트 모델을 학습시켜 보았습니다. 정확도가 99.9%가 나왔습니다.

"와, 대박!" 기뻐하던 것도 잠시, 혼란 행렬을 확인한 순간 얼굴이 굳어졌습니다. 사기 거래 492건 중 무려 150건을 놓쳤던 것입니다.

클래스 불균형 문제는 머신러닝에서 가장 까다로운 문제 중 하나입니다. 마치 시험에서 찍기만 해도 높은 점수가 나오는 것처럼, 모델이 다수 클래스만 예측해도 좋은 성능 지표가 나옵니다.

이런 함정을 피하려면 적절한 평가 지표와 처리 기법이 필요합니다.

다음 코드를 살펴봅시다.

from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix

# 데이터 분리
X = df.drop('Class', axis=1)
y = df['Class']
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 기본 모델 학습 (불균형 그대로)
rf_basic = RandomForestClassifier(random_state=42)
rf_basic.fit(X_train, y_train)
y_pred_basic = rf_basic.predict(X_test)

# 클래스 가중치 적용 모델
rf_weighted = RandomForestClassifier(
    class_weight='balanced',  # 불균형 자동 보정
    random_state=42
)
rf_weighted.fit(X_train, y_train)
y_pred_weighted = rf_weighted.predict(X_test)

# 성능 비교
print("=== 기본 모델 ===")
print(classification_report(y_test, y_pred_basic))
print("\n=== 가중치 적용 모델 ===")
print(classification_report(y_test, y_pred_weighted))

김개발 씨의 표정이 어두워졌습니다. 정확도 99.9%라는 화려한 숫자 뒤에 숨겨진 진실은 참담했습니다.

모델이 사기 거래의 30%나 놓치고 있었던 것입니다. 박시니어 씨가 다가와 화면을 보더니 말했습니다.

"이게 바로 정확도의 역설이에요. 불균형 데이터에서는 정확도가 아무 의미 없어요." 왜 이런 일이 발생하는 걸까요?

이것을 이해하려면 손실 함수의 관점에서 생각해 봐야 합니다. 머신러닝 모델은 학습 과정에서 전체 오류를 최소화하려고 합니다.

그런데 데이터의 99.8%가 정상 거래라면, 모델은 정상 거래를 맞히는 데 집중하게 됩니다. 쉽게 비유하자면, 이것은 마치 선생님이 학생 전체의 평균 점수를 올리려고 할 때와 같습니다.

반에 99명의 우등생과 1명의 부진 학생이 있다면, 선생님은 자연스럽게 우등생들에게 더 신경 쓰게 됩니다. 한 명을 위해 99명을 희생할 수는 없으니까요.

하지만 사기 탐지에서는 그 한 명이 바로 우리가 찾아야 할 대상입니다. 이 문제를 해결하는 가장 간단한 방법이 바로 클래스 가중치 조정입니다.

class_weight='balanced' 옵션을 사용하면 scikit-learn이 자동으로 클래스별 가중치를 계산합니다. 소수 클래스인 사기 거래에는 높은 가중치를, 다수 클래스인 정상 거래에는 낮은 가중치를 부여합니다.

구체적으로 가중치는 n_samples / (n_classes * n_samples_per_class) 공식으로 계산됩니다. 예를 들어 사기 거래가 전체의 0.17%라면, 사기 클래스의 가중치는 정상 클래스의 약 580배가 됩니다.

이렇게 하면 모델이 사기 거래 하나를 놓칠 때 받는 페널티가 정상 거래를 놓칠 때의 580배가 됩니다. 모델은 자연스럽게 사기 거래를 더 신경 쓰게 됩니다.

김개발 씨가 가중치를 적용한 모델을 돌려보았습니다. 놀랍게도 사기 탐지율이 크게 향상되었습니다.

물론 정상 거래를 사기로 잘못 분류하는 경우도 약간 늘어났지만, 전체적인 균형은 훨씬 나아졌습니다. "하지만 선배, 이 방법이 항상 최선인가요?" 김개발 씨가 물었습니다.

박시니어 씨는 고개를 저었습니다. "아니요, 이건 가장 기본적인 방법이에요.

더 정교한 방법들이 있어요. 데이터 자체를 리샘플링하는 방법이죠." 클래스 가중치 조정은 알고리즘 수준의 해결책입니다.

하지만 때로는 데이터 수준에서 직접 불균형을 해결하는 것이 더 효과적일 수 있습니다. 다음 장에서는 바로 그 방법들을 알아보겠습니다.

실전 팁

💡 - class_weight='balanced'는 가장 간단하지만 효과적인 불균형 처리 방법입니다

  • 평가 지표로는 정확도 대신 재현율, 정밀도, F1 점수를 사용하세요

3. 언더샘플링과 오버샘플링

클래스 가중치를 적용한 모델의 성능이 개선되었지만, 김개발 씨는 더 나은 방법이 있을지 궁금해졌습니다. 박시니어 씨가 말했습니다.

"데이터 자체를 조절하는 방법도 있어요. 다수 클래스를 줄이거나, 소수 클래스를 늘리는 거죠." 김개발 씨의 눈이 반짝였습니다.

리샘플링 기법은 데이터 자체의 불균형을 해결하는 방법입니다. 언더샘플링은 다수 클래스의 샘플을 줄이고, 오버샘플링은 소수 클래스의 샘플을 늘립니다.

특히 SMOTE는 단순 복제가 아닌 합성 샘플을 생성하여 과적합을 방지합니다.

다음 코드를 살펴봅시다.

from imblearn.under_sampling import RandomUnderSampler
from imblearn.over_sampling import SMOTE
from collections import Counter

# 원본 데이터 분포 확인
print(f"원본 분포: {Counter(y_train)}")

# 언더샘플링: 다수 클래스를 소수 클래스 수준으로 축소
rus = RandomUnderSampler(random_state=42)
X_under, y_under = rus.fit_resample(X_train, y_train)
print(f"언더샘플링 후: {Counter(y_under)}")

# SMOTE 오버샘플링: 소수 클래스의 합성 샘플 생성
smote = SMOTE(random_state=42)
X_smote, y_smote = smote.fit_resample(X_train, y_train)
print(f"SMOTE 후: {Counter(y_smote)}")

# SMOTE 적용 모델 학습
rf_smote = RandomForestClassifier(random_state=42)
rf_smote.fit(X_smote, y_smote)
y_pred_smote = rf_smote.predict(X_test)

print("\n=== SMOTE 적용 모델 성능 ===")
print(classification_report(y_test, y_pred_smote))

박시니어 씨가 화이트보드에 그림을 그리기 시작했습니다. "김개발 씨, 불균형을 해결하는 방법은 크게 두 가지가 있어요." 첫 번째는 언더샘플링입니다.

많은 쪽을 줄이는 방법이죠. 28만 건의 정상 거래 중에서 492건만 무작위로 선택하면, 사기 거래와 1:1 비율이 됩니다.

"그런데 선배, 그러면 데이터를 너무 많이 버리는 거 아닌가요?" 김개발 씨의 지적은 정확했습니다. 언더샘플링의 가장 큰 단점이 바로 정보 손실입니다.

귀중한 데이터의 99.8%를 버리게 되니까요. 쉽게 비유하자면, 이것은 마치 1000쪽짜리 책에서 2쪽만 읽고 내용을 파악하려는 것과 같습니다.

핵심 내용을 놓칠 수 있습니다. 두 번째 방법은 오버샘플링입니다.

적은 쪽을 늘리는 방법이죠. 492건의 사기 거래를 28만 건으로 늘리면 됩니다.

"그냥 똑같은 데이터를 복사하면 되나요?" 김개발 씨가 물었습니다. 박시니어 씨는 고개를 저었습니다.

"단순 복제는 과적합 문제가 생겨요. 그래서 SMOTE라는 기법을 사용해요." SMOTE(Synthetic Minority Over-sampling Technique)는 합성 샘플을 생성합니다.

기존 사기 거래 샘플과 그 이웃 사이를 보간하여 새로운 데이터 포인트를 만들어내는 것입니다. 예를 들어 사기 거래 A와 그 이웃인 사기 거래 B가 있다고 합시다.

SMOTE는 A와 B 사이의 어딘가에 새로운 점을 찍습니다. 이 점은 A와 B의 특성을 적절히 섞어가진 합성 데이터가 됩니다.

이 방법의 장점은 다양성입니다. 단순히 같은 데이터를 복사하는 것이 아니라, 기존 데이터의 패턴을 학습하여 새로운 변형을 만들어냅니다.

이렇게 하면 모델이 더 일반화된 패턴을 학습할 수 있습니다. 김개발 씨가 코드를 실행해 보았습니다.

SMOTE를 적용하니 학습 데이터의 클래스 비율이 1:1이 되었습니다. 모델을 학습시키자 사기 탐지 성능이 한층 더 향상되었습니다.

"그런데 선배, 언더샘플링이랑 오버샘플링 중에 뭐가 더 좋아요?" 박시니어 씨가 웃으며 대답했습니다. "상황에 따라 달라요.

데이터가 아주 많으면 언더샘플링도 괜찮고, 데이터가 적으면 SMOTE가 나아요. 둘을 섞어 쓰기도 해요." 주의할 점이 있습니다.

리샘플링은 반드시 학습 데이터에만 적용해야 합니다. 테스트 데이터는 실제 상황을 반영해야 하므로 원본 그대로 유지해야 합니다.

이 점을 놓치면 성능 평가가 왜곡됩니다.

실전 팁

💡 - SMOTE는 반드시 학습 데이터에만 적용하고, 테스트 데이터는 원본 그대로 유지하세요

  • imbalanced-learn 라이브러리를 설치하려면 pip install imbalanced-learn 명령을 사용하세요

4. Isolation Forest 적용

지금까지 배운 방법은 모두 지도 학습 기반이었습니다. 라벨이 있는 데이터로 학습하는 방식이죠.

그런데 박시니어 씨가 새로운 아이디어를 꺼냈습니다. "사기 탐지에는 비지도 학습도 잘 작동해요.

특히 Isolation Forest는 이상치 탐지에 아주 효과적이에요."

Isolation Forest는 이상치를 고립시키는 데 필요한 분할 횟수를 기준으로 이상치를 탐지합니다. 마치 숲 속에서 유독 눈에 띄는 나무를 찾아내는 것처럼, 정상 패턴에서 벗어난 거래를 자동으로 식별합니다.

라벨 없이도 학습이 가능하다는 것이 큰 장점입니다.

다음 코드를 살펴봅시다.

from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report

# Amount와 Time 피처 스케일링
scaler = StandardScaler()
X_scaled = X.copy()
X_scaled['Amount'] = scaler.fit_transform(X[['Amount']])
X_scaled['Time'] = scaler.fit_transform(X[['Time']])

# Isolation Forest 모델 학습
iso_forest = IsolationForest(
    n_estimators=100,        # 트리 개수
    contamination=0.002,     # 예상 이상치 비율 (사기 비율과 유사하게)
    random_state=42,
    n_jobs=-1
)

# 학습 및 예측 (-1: 이상치, 1: 정상)
iso_pred = iso_forest.fit_predict(X_scaled)

# 예측값을 0, 1로 변환 (-1 -> 1, 1 -> 0)
iso_pred_binary = (iso_pred == -1).astype(int)

# 성능 평가
print("=== Isolation Forest 성능 ===")
print(classification_report(y, iso_pred_binary))

# 이상치 점수 확인 (낮을수록 이상치)
anomaly_scores = iso_forest.decision_function(X_scaled)
print(f"\n이상치 점수 범위: {anomaly_scores.min():.3f} ~ {anomaly_scores.max():.3f}")

김개발 씨는 Isolation Forest라는 이름이 신기했습니다. "고립시키는 숲이라...

무슨 뜻이에요?" 박시니어 씨가 설명을 시작했습니다. "일반적인 분류 알고리즘은 정상 데이터의 패턴을 학습해요.

하지만 Isolation Forest는 반대로 접근해요. 이상치가 얼마나 쉽게 고립되는지를 측정하는 거예요." 쉽게 비유하자면, 이것은 마치 운동장에서 술래잡기를 하는 것과 같습니다.

대부분의 아이들은 무리 지어 있어서 한 명을 특정하려면 여러 번 그룹을 나눠야 합니다. 하지만 혼자 떨어져 있는 아이는 금방 찾아낼 수 있습니다.

Isolation Forest의 핵심 아이디어가 바로 이것입니다. 정상 데이터는 서로 비슷한 특성을 가지고 뭉쳐 있습니다.

반면 이상치는 다른 데이터와 동떨어져 있습니다. 따라서 무작위로 데이터를 분할할 때, 이상치는 더 적은 분할 횟수로 고립됩니다.

알고리즘은 다음과 같이 동작합니다. 먼저 무작위로 피처를 선택하고, 그 피처의 최소값과 최대값 사이에서 무작위 분할점을 정합니다.

이 과정을 반복하여 각 데이터 포인트가 고립될 때까지 분할합니다. 고립에 필요한 평균 분할 횟수가 바로 이상치 점수가 됩니다.

분할 횟수가 적으면 이상치일 가능성이 높고, 많으면 정상 데이터일 가능성이 높습니다. 김개발 씨가 코드를 실행해 보았습니다.

흥미로운 결과가 나왔습니다. 라벨을 전혀 사용하지 않았는데도 사기 거래를 어느 정도 탐지해 냈습니다.

"와, 라벨 없이도 되네요!" 김개발 씨가 놀랐습니다. 박시니어 씨가 고개를 끄덕였습니다.

"맞아요. 그래서 Isolation Forest는 라벨이 없거나 부족한 상황에서 특히 유용해요." contamination 파라미터는 전체 데이터에서 이상치가 차지하는 비율을 지정합니다.

이 값을 실제 사기 비율과 비슷하게 설정하면 더 좋은 성능을 얻을 수 있습니다. 하지만 Isolation Forest도 만능은 아닙니다.

지도 학습 모델보다 정밀도가 떨어질 수 있습니다. 따라서 실무에서는 Isolation Forest를 1차 필터로 사용하고, 걸러진 데이터를 다시 지도 학습 모델로 검증하는 앙상블 전략을 많이 사용합니다.

김개발 씨는 이 방법이 실제 업무에서 어떻게 활용되는지 조금씩 감이 잡히기 시작했습니다.

실전 팁

💡 - contamination 파라미터는 도메인 지식을 활용하여 실제 이상치 비율에 맞게 조정하세요

  • Isolation Forest는 지도 학습 모델과 함께 앙상블로 사용하면 더 효과적입니다

5. 임계값 조정 전략

모델 학습이 끝났다고 해서 끝이 아닙니다. 김개발 씨는 모델의 예측 결과를 보며 고민에 빠졌습니다.

"0.5를 기준으로 사기/정상을 나누는데, 이게 최선일까요?" 박시니어 씨가 미소 지으며 말했습니다. "좋은 질문이에요.

임계값 조정이야말로 실무에서 가장 중요한 튜닝 포인트예요."

임계값은 확률을 클래스로 변환하는 기준점입니다. 기본값 0.5가 항상 최적은 아닙니다.

사기 탐지처럼 소수 클래스를 놓치면 안 되는 경우, 임계값을 낮춰서 더 많은 거래를 사기 의심 건으로 분류할 수 있습니다.

다음 코드를 살펴봅시다.

from sklearn.metrics import precision_recall_curve, f1_score
import matplotlib.pyplot as plt

# 확률 예측
y_proba = rf_weighted.predict_proba(X_test)[:, 1]

# 다양한 임계값에 대한 성능 계산
thresholds = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]
print("임계값별 성능 비교:")
print("-" * 50)

for thresh in thresholds:
    y_pred_thresh = (y_proba >= thresh).astype(int)

    # 정밀도, 재현율 계산
    tp = ((y_pred_thresh == 1) & (y_test == 1)).sum()
    fp = ((y_pred_thresh == 1) & (y_test == 0)).sum()
    fn = ((y_pred_thresh == 0) & (y_test == 1)).sum()

    precision = tp / (tp + fp) if (tp + fp) > 0 else 0
    recall = tp / (tp + fn) if (tp + fn) > 0 else 0
    f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0

    print(f"임계값 {thresh}: 정밀도={precision:.3f}, 재현율={recall:.3f}, F1={f1:.3f}")

# 최적 임계값 찾기 (F1 기준)
precisions, recalls, threshs = precision_recall_curve(y_test, y_proba)
f1_scores = 2 * precisions * recalls / (precisions + recalls + 1e-10)
best_idx = f1_scores.argmax()
print(f"\n최적 임계값: {threshs[best_idx]:.3f} (F1: {f1_scores[best_idx]:.3f})")

김개발 씨는 모델이 출력하는 숫자를 자세히 살펴보았습니다. 0.73, 0.12, 0.89, 0.45...

이 숫자들은 각 거래가 사기일 확률을 나타냈습니다. "기본적으로 0.5 이상이면 사기로 분류하는데요, 이게 맞는 건가요?" 김개발 씨가 물었습니다.

박시니어 씨가 화이트보드에 직선을 그렸습니다. "0.5는 그냥 관습적인 기준이에요.

사기 탐지에서는 이 기준을 바꿔야 할 때가 많아요." 쉽게 비유하자면, 이것은 마치 체온계의 기준을 정하는 것과 같습니다. 일반적으로 37.5도를 열이 있다고 판단하지만, 전염병 상황에서는 37.0도로 기준을 낮출 수 있습니다.

약간의 미열이라도 놓치지 않기 위해서요. 임계값을 낮추면 어떤 일이 벌어질까요?

0.3만 넘어도 사기로 분류한다면, 더 많은 거래가 사기 의심 건으로 걸리게 됩니다. 실제 사기를 놓칠 확률, 즉 거짓 음성이 줄어듭니다.

하지만 동시에 정상 거래도 많이 걸리게 되어 거짓 양성이 늘어납니다. 반대로 임계값을 높이면 0.7 이상만 사기로 분류합니다.

이러면 확실한 사기만 잡게 되어 거짓 양성은 줄지만, 애매한 사기를 놓칠 수 있어 거짓 음성이 늘어납니다. 김개발 씨가 여러 임계값으로 실험해 보았습니다.

임계값 0.3에서는 재현율이 95%까지 올라갔지만 정밀도가 20%로 떨어졌습니다. 임계값 0.7에서는 정밀도가 85%로 높아졌지만 재현율이 60%로 떨어졌습니다.

"어느 쪽이 맞는 건가요?" 김개발 씨의 질문에 박시니어 씨가 대답했습니다. "비즈니스에 달렸어요.

우리 회사는 사기를 놓치는 것을 더 싫어해요. 고객 피해가 직접적이니까요.

그래서 재현율을 우선시해요." 실무에서는 비용 분석을 통해 최적의 임계값을 결정합니다. 사기 1건을 놓쳤을 때의 평균 손실, 정상 거래 1건을 차단했을 때의 비용을 계산하여 총 비용을 최소화하는 임계값을 찾는 것입니다.

또한 F1 점수를 최대화하는 임계값을 찾는 방법도 많이 사용됩니다. F1 점수는 정밀도와 재현율의 조화 평균으로, 둘 사이의 균형을 나타냅니다.

김개발 씨는 precision_recall_curve 함수를 사용하여 최적의 임계값을 찾아보았습니다. 0.5가 아닌 다른 값에서 F1 점수가 최대가 되는 것을 확인했습니다.

실전 팁

💡 - 임계값 결정은 기술적 문제가 아니라 비즈니스 문제입니다. 담당자와 충분히 논의하세요

  • sklearn의 precision_recall_curve를 활용하면 다양한 임계값에서의 성능을 쉽게 분석할 수 있습니다

6. 정밀도 재현율 트레이드오프

김개발 씨는 임계값을 조정하면서 이상한 현상을 발견했습니다. 재현율을 올리면 정밀도가 떨어지고, 정밀도를 올리면 재현율이 떨어졌습니다.

"왜 둘 다 높일 수는 없나요?" 박시니어 씨가 진지하게 대답했습니다. "그게 바로 머신러닝의 본질적인 한계예요.

정밀도-재현율 트레이드오프라고 해요."

정밀도는 사기라고 예측한 것 중 실제 사기의 비율이고, 재현율은 실제 사기 중 잡아낸 비율입니다. 이 둘은 시소 같은 관계로, 하나를 높이면 다른 하나가 낮아집니다.

완벽한 균형점은 없고, 비즈니스 상황에 맞는 최적점을 찾아야 합니다.

다음 코드를 살펴봅시다.

from sklearn.metrics import precision_recall_curve, average_precision_score
from sklearn.metrics import roc_curve, roc_auc_score

# PR 곡선 데이터 계산
precision, recall, pr_thresholds = precision_recall_curve(y_test, y_proba)
avg_precision = average_precision_score(y_test, y_proba)

# ROC 곡선 데이터 계산
fpr, tpr, roc_thresholds = roc_curve(y_test, y_proba)
roc_auc = roc_auc_score(y_test, y_proba)

print(f"Average Precision (AP): {avg_precision:.4f}")
print(f"ROC AUC Score: {roc_auc:.4f}")

# 특정 재현율에서의 정밀도 확인
target_recalls = [0.7, 0.8, 0.9, 0.95]
print("\n목표 재현율별 정밀도:")
for target in target_recalls:
    idx = (recall >= target).sum() - 1
    if idx >= 0:
        print(f"  재현율 {target*100:.0f}% 달성 시 정밀도: {precision[idx]*100:.1f}%")

# F2 점수 계산 (재현율에 더 가중치)
def f_beta_score(precision, recall, beta=2):
    return (1 + beta**2) * (precision * recall) / (beta**2 * precision + recall + 1e-10)

f2_scores = f_beta_score(precision, recall, beta=2)
best_f2_idx = f2_scores.argmax()
print(f"\n최적 F2 점수: {f2_scores[best_f2_idx]:.4f}")
print(f"  (정밀도: {precision[best_f2_idx]:.3f}, 재현율: {recall[best_f2_idx]:.3f})")

김개발 씨는 고민에 빠졌습니다. 왜 정밀도와 재현율을 동시에 높일 수 없는 걸까요?

박시니어 씨가 쉬운 예를 들어주었습니다. "경찰이 범인을 잡는다고 생각해 봐요.

모든 사람을 다 잡아들이면 범인도 반드시 잡히겠죠? 재현율 100%예요.

하지만 무고한 사람도 많이 잡히니까 정밀도는 바닥이에요." 반대로, 확실한 증거가 있는 사람만 잡으면 어떨까요? 잡은 사람은 거의 다 범인이라 정밀도는 높지만, 증거를 숨긴 범인은 놓치게 됩니다.

재현율이 떨어지는 것입니다. **정밀도(Precision)**는 "내가 사기라고 한 것 중에 진짜 사기가 몇 개인가?"를 나타냅니다.

정밀도가 높으면 거짓 경보가 적습니다. **재현율(Recall)**은 "실제 사기 중에서 내가 몇 개나 잡았는가?"를 나타냅니다.

재현율이 높으면 실제 사기를 놓치는 경우가 적습니다. 사기 탐지에서는 보통 재현율을 더 중시합니다.

사기를 놓치면 고객이 금전적 피해를 입기 때문입니다. 정상 거래를 잘못 차단하면 고객이 불편하겠지만, 전화 한 통이면 해결됩니다.

이런 상황에서는 F2 점수가 유용합니다. F1 점수는 정밀도와 재현율에 동등한 가중치를 주지만, F2 점수는 재현율에 더 큰 가중치를 줍니다.

사기 탐지처럼 놓치는 것이 치명적인 경우에 적합합니다. 김개발 씨가 PR 곡선을 그려보았습니다.

곡선이 오른쪽 위로 갈수록 좋은 모델입니다. 이상적인 모델은 정밀도와 재현율이 모두 1인 점, 즉 오른쪽 위 꼭짓점에 있을 것입니다.

**Average Precision(AP)**은 PR 곡선 아래의 면적입니다. 이 값이 클수록 전반적으로 좋은 성능을 보이는 모델입니다.

불균형 데이터에서는 ROC AUC보다 AP가 더 신뢰할 수 있는 지표입니다. "선배, 그럼 실무에서는 어떻게 결정해요?" 김개발 씨가 물었습니다.

박시니어 씨가 대답했습니다. "보통 목표 재현율을 먼저 정해요.

예를 들어 '사기의 90% 이상은 반드시 잡아야 한다'라고요. 그 조건을 만족하는 선에서 정밀도를 최대화하는 거예요." 김개발 씨는 이제 사기 탐지 모델의 전체 그림이 그려지기 시작했습니다.

단순히 모델을 학습시키는 것이 아니라, 데이터의 특성을 이해하고, 불균형을 처리하고, 적절한 평가 지표와 임계값을 선택하는 것까지가 사기 탐지의 완전한 파이프라인이었습니다. "감사합니다, 선배.

이제 어떻게 해야 할지 알겠어요!" 김개발 씨의 눈이 자신감으로 빛났습니다.

실전 팁

💡 - 불균형 데이터에서는 ROC AUC보다 **Average Precision(AP)**이 더 신뢰할 수 있는 평가 지표입니다

  • 재현율을 중시하는 경우 F1 대신 F2 점수를 사용하세요 (beta=2로 설정)

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#MachineLearning#FraudDetection#ImbalancedData#IsolationForest#SMOTE#Machine Learning,Python

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스