포스트

[Python 100일 챌린지] Day 95 - 분류 모델 (로지스틱 회귀)

[Python 100일 챌린지] Day 95 - 분류 모델 (로지스틱 회귀)

Yes or No, 합격 or 불합격! 로지스틱 회귀는 분류 문제의 기본 중의 기본입니다. 이메일이 스팸인지, 대출이 승인될지, 질병이 있는지… 이진 분류는 모두 여기서 시작합니다. 이름은 ‘회귀’지만 실제로는 ‘분류’를 한다는 사실!

(30분 완독 ⭐⭐⭐)

🎯 오늘의 학습 목표

📚 사전 지식

  • Day 94: 선형 회귀 모델
  • Day 93: 데이터 전처리 기초
  • Day 91: 회귀 vs 분류의 차이

🎯 학습 목표 1: 로지스틱 회귀의 원리 이해하기

💡 “회귀인데 분류라고?” 헷갈리죠? 처음엔 다 그래요! 이름 때문에 혼란스러운데, 오늘 배우고 나면 완전히 이해됩니다. 걱정 마세요! 😊

선형 회귀 vs 로지스틱 회귀

1
2
3
4
5
6
7
8
# 선형 회귀 (숫자 예측)
공부시간 = 5
예측점수 = 75.3  # 연속적인 숫자

# 로지스틱 회귀 (범주 예측)
공부시간 = 5
합격확률 = 0.85  # 0~1 사이 확률
합격여부 = "합격" if 합격확률 > 0.5 else "불합격"

실생활 비유

로지스틱 회귀 = 입장 게이트 🚪

놀이공원 입구를 생각해보세요!

1
2
3
4
5
6
키 측정 → 확률 계산 → 게이트 결정

120cm → 10% → "탑승 불가" ❌
130cm → 45% → "탑승 불가" ❌
140cm → 65% → "탑승 가능" ✅
150cm → 90% → "탑승 가능" ✅

핵심:

  • 키 (연속적 숫자) → 확률 (0~1) → 결정 (Yes/No)
  • 중간 어딘가(50%)가 기준선!
  • 기준선 위면 통과, 아래면 탈락

실제 응용:

  • 🏦 은행: “신용점수 → 대출 승인/거부”
  • 📧 이메일: “단어 빈도 → 스팸/정상”
  • 🏥 병원: “검사 수치 → 질병 있음/없음”

시그모이드 함수

무한대를 0~1 사이로 압축

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np
import matplotlib.pyplot as plt

# 시그모이드 함수
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# 그래프 그리기
x = np.linspace(-10, 10, 100)
y = sigmoid(x)

plt.figure(figsize=(10, 6))
plt.plot(x, y, linewidth=3, color='blue')
plt.axhline(y=0.5, color='red', linestyle='--', label='기준선 (0.5)')
plt.xlabel('x', fontsize=12)
plt.ylabel('sigmoid(x)', fontsize=12)
plt.title('시그모이드 함수: 확률을 0~1로 변환', fontsize=14)
plt.grid(True, alpha=0.3)
plt.legend()
plt.savefig('sigmoid_function.png', dpi=150, bbox_inches='tight')
print("그래프 저장: sigmoid_function.png")

특징:

  • x가 매우 크면 → y는 1에 가까움 (거의 확실히 1)
  • x가 매우 작으면 → y는 0에 가까움 (거의 확실히 0)
  • x가 0이면 → y는 0.5 (반반)

로지스틱 회귀의 작동 원리

1
2
3
4
5
6
7
8
9
10
11
# 1. 선형 결합 계산
z = w1*x1 + w2*x2 + b

# 2. 시그모이드 함수 적용
확률 = 1 / (1 + exp(-z))

# 3. 임계값으로 분류
if 확률 >= 0.5:
    결과 = 1 (긍정)
else:
    결과 = 0 (부정)

🎯 학습 목표 2: LogisticRegression으로 분류 모델 만들기

기본 사용법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
import numpy as np

# 데이터: 공부시간 vs 합격여부
study_hours = np.array([1, 2, 3, 4, 5, 6, 7, 8]).reshape(-1, 1)
passed = np.array([0, 0, 0, 0, 1, 1, 1, 1])  # 0=불합격, 1=합격

# 모델 생성 및 학습
model = LogisticRegression()
model.fit(study_hours, passed)

# 확률 예측
probabilities = model.predict_proba(study_hours)
print("공부시간 | 불합격 확률 | 합격 확률")
print("-" * 40)
for hours, prob in zip(study_hours, probabilities):
    print(f"  {hours[0]}시간   |    {prob[0]:.2f}    |   {prob[1]:.2f}")

출력:

1
2
3
4
5
6
7
8
9
10
공부시간 | 불합격 확률 | 합격 확률
----------------------------------------
  1시간   |    0.96    |   0.04
  2시간   |    0.88    |   0.12
  3시간   |    0.69    |   0.31
  4시간   |    0.43    |   0.57
  5시간   |    0.20    |   0.80
  6시간   |    0.07    |   0.93
  7시간   |    0.02    |   0.98
  8시간   |    0.01    |   0.99

실전 예제: 대출 승인 예측

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import numpy as np

# 데이터: [연봉(만원), 신용점수] vs 승인여부
X = np.array([
    [3000, 650],  # 연봉 3000만원, 신용점수 650
    [4000, 700],
    [5000, 750],
    [6000, 800],
    [3500, 600],
    [4500, 720],
    [5500, 780],
    [6500, 850]
])
y = np.array([0, 0, 1, 1, 0, 1, 1, 1])  # 0=거부, 1=승인

# 데이터 분리
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42
)

# 스케일링 (중요!)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# 모델 학습
model = LogisticRegression()
model.fit(X_train_scaled, y_train)

# 예측
y_pred = model.predict(X_test_scaled)
y_proba = model.predict_proba(X_test_scaled)

# 결과 출력
print("테스트 결과:")
print("연봉   | 신용점수 | 실제 | 예측 | 승인확률")
print("-" * 55)
for features, actual, pred, proba in zip(X_test, y_test, y_pred, y_proba):
    result = "승인" if pred == 1 else "거부"
    actual_result = "승인" if actual == 1 else "거부"
    print(f"{features[0]:5.0f} | {features[1]:8.0f} | {actual_result:4s} | {result:4s} | {proba[1]:.2%}")

# 정확도
accuracy = model.score(X_test_scaled, y_test)
print(f"\n정확도: {accuracy * 100:.2f}%")

출력:

1
2
3
4
5
6
7
테스트 결과:
연봉   | 신용점수 | 실제 | 예측 | 승인확률
-------------------------------------------------------
 5000 |      750 | 승인 | 승인 | 78.42%
 3500 |      600 | 거부 | 거부 | 15.23%

정확도: 100.00%

새로운 신청자 예측

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 새로운 대출 신청자
new_applicants = np.array([
    [4200, 680],  # 연봉 4200만원, 신용점수 680
    [5800, 820]   # 연봉 5800만원, 신용점수 820
])

# 스케일링 (훈련 데이터의 스케일러 사용!)
new_applicants_scaled = scaler.transform(new_applicants)

# 예측
predictions = model.predict(new_applicants_scaled)
probabilities = model.predict_proba(new_applicants_scaled)

print("\n신규 신청자 예측:")
for applicant, pred, proba in zip(new_applicants, predictions, probabilities):
    result = "승인" if pred == 1 else "거부"
    print(f"연봉: {applicant[0]:.0f}만원, 신용점수: {applicant[1]:.0f}")
    print(f"→ 결과: {result}, 승인 확률: {proba[1]:.2%}\n")

출력:

1
2
3
4
5
6
신규 신청자 예측:
연봉: 4200만원, 신용점수: 680
→ 결과: 거부, 승인 확률: 45.32%

연봉: 5800만원, 신용점수: 820
→ 결과: 승인, 승인 확률: 92.15%

🎯 학습 목표 3: 분류 모델 성능 평가하기

Accuracy (정확도)

전체 중 맞춘 비율

1
2
3
4
from sklearn.metrics import accuracy_score

accuracy = accuracy_score(y_test, y_pred)
print(f"정확도: {accuracy * 100:.2f}%")

Confusion Matrix (혼동 행렬)

어떻게 틀렸는지 자세히

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from sklearn.metrics import confusion_matrix
import seaborn as sns
import matplotlib.pyplot as plt

# 혼동 행렬 계산
cm = confusion_matrix(y_test, y_pred)

# 시각화
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=['거부', '승인'],
            yticklabels=['거부', '승인'])
plt.ylabel('실제값')
plt.xlabel('예측값')
plt.title('혼동 행렬')
plt.savefig('confusion_matrix.png', dpi=150, bbox_inches='tight')
print("혼동 행렬 저장: confusion_matrix.png")

해석:

1
2
3
4
5
6
7
8
              예측: 거부  예측: 승인
실제: 거부      TN         FP
실제: 승인      FN         TP

TN (True Negative): 거부를 거부로 맞춤
TP (True Positive): 승인을 승인으로 맞춤
FP (False Positive): 거부인데 승인으로 예측 (Type 1 오류)
FN (False Negative): 승인인데 거부로 예측 (Type 2 오류)

Precision, Recall, F1-Score

1
2
3
4
5
6
from sklearn.metrics import classification_report

# 상세 리포트
report = classification_report(y_test, y_pred,
                              target_names=['거부', '승인'])
print(report)

출력:

1
2
3
4
5
6
7
8
              precision    recall  f1-score   support

        거부       1.00      1.00      1.00         1
        승인       1.00      1.00      1.00         1

    accuracy                           1.00         2
   macro avg       1.00      1.00      1.00         2
weighted avg       1.00      1.00      1.00         2

용어 설명:

  • Precision (정밀도): 승인이라고 예측한 것 중 실제 승인 비율
    • TP / (TP + FP)
    • “양성 예측의 정확성”
  • Recall (재현율): 실제 승인 중 승인으로 예측한 비율
    • TP / (TP + FN)
    • “실제 양성을 찾아내는 능력”
  • F1-Score: Precision과 Recall의 조화 평균
    • 2 * (Precision * Recall) / (Precision + Recall)

💻 실전 예제: 타이타닉 생존 예측

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
import numpy as np

# 타이타닉 데이터 (간소화)
# [나이, 성별(0=여, 1=남), 객실등급(1~3)]
X = np.array([
    [22, 1, 3],  # 22세, 남자, 3등실
    [38, 0, 1],  # 38세, 여자, 1등실
    [26, 0, 3],
    [35, 1, 1],
    [35, 0, 3],
    [54, 1, 1],
    [2, 0, 3],
    [27, 1, 2],
    [14, 0, 2],
    [4, 1, 3]
])

# 생존 여부 (0=사망, 1=생존)
y = np.array([0, 1, 1, 1, 0, 1, 1, 0, 1, 0])

# 데이터 분리
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42
)

# 모델 학습
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)

# 예측
y_pred = model.predict(X_test)

# 평가
print("타이타닉 생존 예측 결과:")
print(classification_report(y_test, y_pred,
                           target_names=['사망', '생존']))

# 새로운 승객 예측
new_passengers = np.array([
    [30, 0, 1],  # 30세, 여자, 1등실
    [25, 1, 3]   # 25세, 남자, 3등실
])

predictions = model.predict(new_passengers)
probabilities = model.predict_proba(new_passengers)

print("\n신규 승객 예측:")
for passenger, pred, proba in zip(new_passengers, predictions, probabilities):
    age, gender, pclass = passenger
    gender_str = "남자" if gender == 1 else "여자"
    result = "생존" if pred == 1 else "사망"
    print(f"{age}{gender_str}, {pclass}등실 → {result} (생존 확률: {proba[1]:.2%})")

📊 로지스틱 회귀 vs 다른 분류 알고리즘

알고리즘 장점 단점 사용 시기
로지스틱 회귀 빠름, 해석 쉬움 선형 경계만 빠른 프로토타입
의사결정나무 해석 쉬움 과적합 위험 규칙 기반
랜덤 포레스트 정확도 높음 느림, 해석 어려움 정확도 중요
SVM 비선형 경계 느림, 파라미터 조정 고차원 데이터

⚠️ 주의사항

1. 클래스 불균형 문제

1
2
3
4
5
# 데이터가 불균형하면
y = [0, 0, 0, 0, 0, 0, 0, 0, 0, 1]  # 90% vs 10%

# 해결: class_weight 파라미터
model = LogisticRegression(class_weight='balanced')

2. 스케일링 필수

1
2
3
4
5
# 로지스틱 회귀는 스케일에 민감!
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

3. 확률 vs 예측

1
2
3
4
5
# predict(): 0 또는 1
prediction = model.predict(X)  # [0, 1, 1]

# predict_proba(): 각 클래스 확률
probability = model.predict_proba(X)  # [[0.8, 0.2], [0.3, 0.7], ...]

📝 요약

  1. 로지스틱 회귀: 이진 분류의 기본 알고리즘
  2. 시그모이드 함수: 선형 결과를 0~1 확률로 변환
  3. 사용법: LogisticRegression().fit(X, y)
  4. 평가: Accuracy, Precision, Recall, F1-Score
  5. 주의: 스케일링 필수, 클래스 불균형 고려

🧪 연습 문제

문제: 스팸 메일 분류기

이메일 특징으로 스팸을 분류하는 모델을 만들어보세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
# [링크개수, 대문자비율, 느낌표개수]
X = np.array([
    [5, 80, 10],   # 스팸
    [0, 20, 1],    # 정상
    [8, 90, 15],   # 스팸
    [1, 10, 0],    # 정상
    [3, 50, 5]     # ?
])
y = np.array([1, 0, 1, 0])  # 1=스팸, 0=정상

# TODO:
# 1. 마지막 이메일([3, 50, 5])이 스팸인지 예측
# 2. 스팸 확률 출력
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from sklearn.linear_model import LogisticRegression
import numpy as np

X_train = np.array([[5, 80, 10], [0, 20, 1], [8, 90, 15], [1, 10, 0]])
y_train = np.array([1, 0, 1, 0])
X_test = np.array([[3, 50, 5]])

# 모델 학습
model = LogisticRegression()
model.fit(X_train, y_train)

# 예측
prediction = model.predict(X_test)
probability = model.predict_proba(X_test)

result = "스팸" if prediction[0] == 1 else "정상"
spam_prob = probability[0][1]

print(f"예측: {result}")
print(f"스팸 확률: {spam_prob:.2%}")

🤔 자주 묻는 질문 (FAQ)

Q1: 왜 이름이 “회귀”인데 분류를 하나요?

A: 역사적인 이유입니다! 🤔

이름의 유래:

  1. 선형 회귀처럼 선형 결합을 사용 (w1x1 + w2x2 + b)
  2. 그 결과를 시그모이드 함수로 변환 (0~1)
  3. 임계값(0.5)으로 분류
1
2
3
4
5
6
7
8
# 내부적으로는 "회귀"
z = w1*x1 + w2*x2 + b  # 선형 회귀와 같음

# 시그모이드로 확률 변환
probability = 1 / (1 + exp(-z))

# 최종 출력은 "분류"
class = 1 if probability >= 0.5 else 0

비유로 이해:

  • 피자는 “파이(pie)”에서 유래했지만 파이가 아닙니다!
  • 로지스틱 회귀도 “회귀”에서 유래했지만 분류 알고리즘입니다!

실전 팁: 혼동하지 말고 “로지스틱 회귀 = 분류”로 외우세요!

Q2: predict()와 predict_proba()의 차이가 뭔가요?

A: 하나는 결정, 하나는 확률입니다! 📊

실생활 예시: 대학 입학

1
2
3
4
5
6
7
8
9
10
# 학생 데이터
student = [[90]]  # 성적 90점

# predict(): "합격" or "불합격" (결정)
result = model.predict(student)
print(result)  # [1] → 합격!

# predict_proba(): 합격 확률 (확률)
proba = model.predict_proba(student)
print(proba)  # [[0.15, 0.85]] → 불합격 15%, 합격 85%

자세한 설명:

predict(): 최종 결정만

1
2
3
predictions = model.predict(X)
# [0, 1, 1, 0, 1]
# 0 = 클래스 0, 1 = 클래스 1

predict_proba(): 각 클래스의 확률

1
2
3
4
probabilities = model.predict_proba(X)
# [[0.8, 0.2],   # 클래스 0: 80%, 클래스 1: 20%
#  [0.3, 0.7],   # 클래스 0: 30%, 클래스 1: 70%
#  [0.1, 0.9]]   # 클래스 0: 10%, 클래스 1: 90%

언제 무엇을 사용?

상황 사용 함수
최종 결정만 필요 predict()
확신도가 궁금함 predict_proba()
임계값 조정 필요 predict_proba()
비즈니스 의사결정 predict_proba()

실전 예시:

1
2
3
4
5
6
7
# 대출 승인: 확률이 70% 이상일 때만 승인
proba = model.predict_proba(X_test)[:, 1]  # 승인 확률만
approval = (proba >= 0.7).astype(int)  # 70% 기준

# 의료 진단: 확률 50% 이상이면 위험
risk_proba = model.predict_proba(X_test)[:, 1]
high_risk = risk_proba >= 0.5

Q3: 임계값을 0.5가 아닌 다른 값으로 바꿀 수 있나요?

A: 네! 상황에 따라 바꿔야 할 때도 많습니다! ⚖️

기본값 (0.5):

1
2
# 확률 50% 이상 → 클래스 1
prediction = model.predict(X)

임계값 변경이 필요한 경우:

사례 1: 스팸 필터 (False Positive 최소화)

1
2
3
4
5
6
7
8
9
# 정상 메일을 스팸으로 분류하면 큰 문제!
# → 임계값을 높여서 확실할 때만 스팸으로 분류

proba = model.predict_proba(X)[:, 1]  # 스팸 확률
spam = (proba >= 0.8).astype(int)  # 80% 이상일 때만 스팸

# 결과:
# - 스팸 놓칠 수 있음 (괜찮음)
# - 정상을 스팸으로 안 함 (중요!)

사례 2: 질병 진단 (False Negative 최소화)

1
2
3
4
5
6
7
8
9
# 질병 있는데 없다고 하면 생명에 위협!
# → 임계값을 낮춰서 의심스러우면 양성 판정

proba = model.predict_proba(X)[:, 1]  # 질병 확률
disease = (proba >= 0.3).astype(int)  # 30%만 넘어도 양성

# 결과:
# - 거짓 양성 많아짐 (재검사하면 됨)
# - 질병 놓치지 않음 (중요!)

임계값 선택 가이드:

상황 임계값 이유
균형 0.5 기본값, 양쪽 동등
보수적 양성 0.7~0.8 FP 피하고 싶을 때
적극적 양성 0.3~0.4 FN 피하고 싶을 때

코드 예시:

1
2
3
4
5
6
7
8
# 여러 임계값 테스트
thresholds = [0.3, 0.5, 0.7]
proba = model.predict_proba(X_test)[:, 1]

for threshold in thresholds:
    predictions = (proba >= threshold).astype(int)
    accuracy = accuracy_score(y_test, predictions)
    print(f"임계값 {threshold}: 정확도 {accuracy:.2%}")

Q4: Precision과 Recall을 쉽게 설명해주세요!

A: 실생활 비유로 쉽게 이해해보죠! 🎯

상황: 경찰이 범죄자를 잡는다

Precision (정밀도): “체포한 사람 중 진짜 범죄자 비율”

1
2
3
4
5
6
Precision = TP / (TP + FP)

# 100명 체포 → 90명이 진짜 범죄자
# Precision = 90/100 = 0.9 (90%)

의미: "얼마나 정확하게 잡았는가?"

Recall (재현율): “진짜 범죄자 중 잡은 비율”

1
2
3
4
5
6
Recall = TP / (TP + FN)

# 진짜 범죄자 100명 → 70명 잡음
# Recall = 70/100 = 0.7 (70%)

의미: "얼마나 많이 찾아냈는가?"

트레이드오프:

1
2
3
4
5
6
7
엄격한 기준 (임계값 높음)
→ Precision ⬆ Recall ⬇
→ "확실할 때만 체포" (무고한 사람 적음, 놓치는 범죄자 많음)

느슨한 기준 (임계값 낮음)
→ Precision ⬇ Recall ⬆
→ "의심되면 체포" (무고한 사람 많음, 놓치는 범죄자 적음)

실전 예시:

스팸 필터 (Precision 중요):

1
2
3
4
5
# 정상 메일을 스팸으로 분류하면 큰일!
# → Precision을 높여야 함

Precision = 0.95 (좋음!)  # 스팸이라고 한 게 95% 진짜 스팸
Recall = 0.60 (괜찮음)     # 스팸 60%만 잡음 (나머지는 받은편지함)

암 진단 (Recall 중요):

1
2
3
4
5
# 암 환자를 놓치면 생명에 위협!
# → Recall을 높여야 함

Precision = 0.70 (괜찮음)  # 양성 판정 중 70%가 진짜 암
Recall = 0.95 (좋음!)      # 암 환자 95% 발견 (5%만 놓침)

F1-Score: 둘의 균형

1
2
3
4
F1 = 2 * (Precision * Recall) / (Precision + Recall)

# Precision과 Recall이 모두 높아야 F1도 높음
# 둘 중 하나라도 낮으면 F1 낮아짐

Q5: 데이터가 불균형하면 어떻게 하나요?

A: 여러 방법이 있습니다! 실무에서 매우 흔한 문제예요! ⚖️

문제 상황:

1
2
3
4
5
6
# 신용카드 사기 탐지
정상 거래: 9,900 (99%)
사기 거래: 100 (1%)

# 모델이 항상 "정상"이라고 하면?
# 정확도 99%! (하지만 쓸모없음)

해결 방법들:

방법 1: class_weight=’balanced’ (가장 쉬움!)

1
2
3
4
5
6
7
# scikit-learn이 자동으로 가중치 조정
model = LogisticRegression(class_weight='balanced')
model.fit(X_train, y_train)

# 내부적으로:
# - 소수 클래스에 더 큰 가중치
# - 다수 클래스에 작은 가중치

방법 2: 오버샘플링 (SMOTE)

1
2
3
4
5
6
7
8
9
10
from imblearn.over_sampling import SMOTE

# 소수 클래스를 늘림 (합성 데이터 생성)
smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)

print("원래:", Counter(y_train))      # {0: 9900, 1: 100}
print("변환:", Counter(y_resampled))  # {0: 9900, 1: 9900}

model.fit(X_resampled, y_resampled)

방법 3: 언더샘플링

1
2
3
4
5
6
7
8
from imblearn.under_sampling import RandomUnderSampler

# 다수 클래스를 줄임
rus = RandomUnderSampler(random_state=42)
X_resampled, y_resampled = rus.fit_resample(X_train, y_train)

print("변환:", Counter(y_resampled))  # {0: 100, 1: 100}
# 주의: 데이터 버림!

방법 4: 평가 지표 변경

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Accuracy 대신 다른 지표 사용

from sklearn.metrics import (
    precision_score,
    recall_score,
    f1_score,
    roc_auc_score
)

# F1-Score (균형)
f1 = f1_score(y_test, y_pred)

# ROC-AUC (불균형에 강함)
auc = roc_auc_score(y_test, y_proba)

print(f"F1-Score: {f1:.3f}")
print(f"ROC-AUC: {auc:.3f}")

실전 가이드:

불균형 비율 해결책
90:10 class_weight=’balanced’
95:5 SMOTE + class_weight
99:1 SMOTE + 평가지표 변경

주의사항:

1
2
3
4
5
6
7
8
9
10
11
# ❌ 잘못된 방법
# train/test 분리 전에 오버샘플링
X_resampled, y_resampled = smote.fit_resample(X, y)
X_train, X_test = train_test_split(X_resampled, y_resampled)
# → 데이터 누수!

# ✅ 올바른 방법
# train/test 분리 후 train만 오버샘플링
X_train, X_test, y_train, y_test = train_test_split(X, y)
X_train_resampled, y_train_resampled = smote.fit_resample(X_train, y_train)
model.fit(X_train_resampled, y_train_resampled)

Q6: 로지스틱 회귀가 잘 안 맞는 경우는 언제인가요?

A: 데이터가 선형적으로 분리 안 될 때입니다! 📉

로지스틱 회귀의 가정:

  • 특징들의 선형 결합으로 클래스를 구분할 수 있음
  • 직선(2D) 또는 평면(3D)으로 나눌 수 있음

잘 작동하는 경우 ✅:

1
2
3
4
5
6
7
8
9
# XOR 문제가 아닌 경우
# 선형으로 분리 가능

   Class 0    |    Class 1
      ●●●     |     ○○○
      ●●●     |     ○○○
      ●●●     |     ○○○
   -----------|-----------
         (직선으로 분리 가능!)

잘 안 되는 경우 ❌:

1
2
3
4
5
6
7
8
9
10
11
# XOR 문제
        
        

# 원형 데이터
      ○○○○○
    ○●●●●●○
    ○●●●●●○
      ○○○○○

# 직선으로 분리 불가능!

해결 방법:

방법 1: 다항 특징 추가

1
2
3
4
5
6
7
8
from sklearn.preprocessing import PolynomialFeatures

# x, y → x, y, x², xy, y²
poly = PolynomialFeatures(degree=2)
X_poly = poly.fit_transform(X)

model = LogisticRegression()
model.fit(X_poly, y)

방법 2: 다른 알고리즘 사용

1
2
3
4
5
6
7
8
9
10
# 비선형 경계를 그릴 수 있는 모델

from sklearn.svm import SVC
model = SVC(kernel='rbf')  # 커널 트릭

from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier()  # 트리 기반

from sklearn.neural_network import MLPClassifier
model = MLPClassifier()  # 신경망

확인 방법:

1
2
3
4
5
6
7
8
9
10
11
# 산점도로 확인
import matplotlib.pyplot as plt

plt.scatter(X[y==0, 0], X[y==0, 1], c='blue', label='Class 0')
plt.scatter(X[y==1, 0], X[y==1, 1], c='red', label='Class 1')
plt.legend()
plt.show()

# 직선으로 나눌 수 있나요?
# → Yes: 로지스틱 회귀 OK
# → No: 다른 방법 시도

실전 팁:

  1. 일단 로지스틱 회귀로 baseline 만들기
  2. 정확도 < 70%이면 산점도 확인
  3. 비선형 패턴이면 다른 모델 시도
  4. 복잡한 모델보다 특징 엔지니어링이 더 효과적일 수 있음!

📚 다음 학습

Day 96: 모델 평가하기 ⭐⭐⭐

내일은 모델을 제대로 평가하는 방법을 배웁니다. 교차 검증, ROC 곡선 등!


“로지스틱 회귀는 ‘회귀’라는 이름이 붙었지만 분류 알고리즘입니다. 혼동하지 마세요!” 🚀

Day 95/100 Phase 10: AI/ML 입문 #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.