분류 모델의 성능이 기대에 못 미칠 때, 모델을 바꾸기 전에 할 수 있는 일이 많습니다. 이 가이드는 모델은 그대로 두고 성능을 올리는 방법을 다룹니다. 점수를 믿을 수 있게 평가를 바로잡고(1), 가중치를 찾아가는 학습 과정을 조정하고(2), 특성을 만들되 과적합을 규제하고(3), 마지막으로 여러 모델을 묶습니다(4).
● 클래스 A · ○ 클래스 B · 점선 밑줄 단어는 눌러서 비유 보기
쉬운 설명판에는 장마다 "물고기로 보면" 칸이 있어, 잡은 물고기의 무게·길이·높이·두께로 종을 맞히는 문제에 같은 방법을 실제로 적용한 결과를 보여 줍니다. 어려운 단어는 점선 밑줄을 누르면 비유로 볼 수 있고, 맨 뒤에 용어 사전이 있습니다.
모델을 바꾸기 전에 확인할 네 곳이 있습니다. 증상으로 해당 장을 찾으세요. 낯선 용어는 점선 밑줄을 누르거나 맨 뒤 용어 사전에서 볼 수 있습니다.
개선은 "무엇이 문제인지"를 먼저 알아야 방향이 정해집니다. 기준선 점수를 적어 둔 뒤, 아래 증상 중 어디에 해당하는지 찾아 해당 장으로 가면 됩니다.
| 이런 증상이 보이면 | 먼저 볼 곳 | 고치는 대상 |
|---|---|---|
| 나눌 때마다 점수가 크게 달라진다 | 1. 데이터 분할 | 점수를 믿을 수 있게 |
| 정확도는 높은데 소수 클래스를 거의 못 찾는다 | 1. 불균형과 지표 | 무엇을 잴지 |
| 손실이 줄지 않거나, 튀거나, 너무 느리게 준다 | 2. 학습 과정 | 가중치를 찾는 과정 |
| 학습 점수와 시험 점수가 둘 다 낮다 | 3. 특성 생성 | 모델이 볼 재료 |
| 학습 점수는 높은데 시험 점수가 낮다 | 3. 규제 | 모델의 복잡도 |
| 모델 하나의 판단이 불안정하거나 한쪽으로 치우친다 | 4. 여러 모델 혼합 | 판단을 묶는 방식 |
점수가 모델의 실력인지, 나눈 운인지부터 가립니다.
쉽게 말하면반 대표를 뽑는 시험에서 문제지를 딱 한 장만 뽑으면, 우연히 쉬운 문제가 걸린 학생이 뽑힐 수 있습니다. 문제지를 바꿔 가며 여러 번 보고 평균을 내면 운이 걸러집니다. 이것이 교차검증입니다. 층화 추출(stratify)은 여론조사에서 연령대 비율을 인구와 똑같이 맞춰 표본을 뽑는 것과 같습니다.
Pipeline 안에 넣어 학습 조각에서만 계산하게 합니다.GroupKFold를 씁니다.TimeSeriesSplit으로 과거→미래 순서를 지킵니다.①에서 지금 나눈 시험 세트를 네 칸으로 나눠 셉니다. 소수 클래스를 찾아야 할 대상(양성)으로 봅니다.
실제 데이터에서도 같은 일이 일어납니다. 아래 ⑤ 코드의 결과에서 기본 로지스틱 회귀는 정확도 0.917이지만 소수 클래스 재현율은 0.357입니다. class_weight="balanced"로 소수 클래스에 무게를 주면 재현율이 0.700으로 오르고 정확도는 0.823으로 내려갑니다. 무엇을 우선할지(놓치지 않기 vs 헛경보 줄이기)는 문제에 따라 정합니다.
# 1. 데이터 분할: stratify와 교차검증
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
# 소수 클래스가 10%인 불균형 데이터
X, y = make_classification(n_samples=300, n_features=6, n_informative=4,
weights=[0.9, 0.1], random_state=0)
# (1) stratify 없이 20번 나누기 vs stratify로 20번 나누기: 테스트 세트의 소수 클래스 비율
plain, strat = [], []
for seed in range(20):
_, _, _, y_te = train_test_split(X, y, test_size=0.2, random_state=seed)
plain.append(y_te.mean())
_, _, _, y_te = train_test_split(X, y, test_size=0.2, random_state=seed, stratify=y)
strat.append(y_te.mean())
print(f"전체 소수 비율 {y.mean():.3f}")
print(f"stratify 없음: 테스트 소수 비율 {min(plain):.3f} ~ {max(plain):.3f}")
print(f"stratify 사용: 테스트 소수 비율 {min(strat):.3f} ~ {max(strat):.3f}")
# (2) 한 번 나눈 점수는 분할 운에 따라 흔들린다
model = make_pipeline(StandardScaler(), LogisticRegression()) # 스케일링을 파이프라인 안에: 누수 방지
single = []
for seed in range(20):
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=seed, stratify=y)
single.append(model.fit(X_tr, y_tr).score(X_te, y_te))
print(f"한 번 분할 정확도 20회: {min(single):.3f} ~ {max(single):.3f}")
# (3) 층화 5겹 교차검증: 평균과 표준편차로 보고
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
acc = cross_val_score(model, X, y, cv=cv, scoring="accuracy")
rec = cross_val_score(model, X, y, cv=cv, scoring="recall")
print(f"5겹 정확도 {acc.mean():.3f} ± {acc.std():.3f}")
print(f"5겹 재현율(소수 클래스) {rec.mean():.3f} ± {rec.std():.3f}")
# (4) 정확도의 함정: 전부 다수 클래스라고 답하는 모델
print(f"모두 다수 클래스로 찍어도 정확도 {1 - y.mean():.3f}, 재현율 0.000")
# (5) 불균형 보정: 소수 클래스에 더 큰 가중치
balanced = make_pipeline(StandardScaler(), LogisticRegression(class_weight="balanced"))
rec_b = cross_val_score(balanced, X, y, cv=cv, scoring="recall")
acc_b = cross_val_score(balanced, X, y, cv=cv, scoring="accuracy")
print(f"class_weight='balanced': 재현율 {rec_b.mean():.3f}, 정확도 {acc_b.mean():.3f}")실행 결과 (scikit-learn 1.8, CPU)
전체 소수 비율 0.113 stratify 없음: 테스트 소수 비율 0.050 ~ 0.183 stratify 사용: 테스트 소수 비율 0.117 ~ 0.117 한 번 분할 정확도 20회: 0.867 ~ 0.967 5겹 정확도 0.917 ± 0.018 5겹 재현율(소수 클래스) 0.357 ± 0.128 모두 다수 클래스로 찍어도 정확도 0.887, 재현율 0.000 class_weight='balanced': 재현율 0.700, 정확도 0.823
이 물고기 1,554마리는 Kaggle의 Fish Market 원본 159마리를 정규분포를 고려해 증강한 것이라, 같은 원본에서 나온 물고기가 9~10마리씩 있습니다(같은 seed_id). 마리 단위로 섞어 나누면 시험 세트의 물고기와 거의 똑같은 "쌍둥이"가 학습 세트에 이미 들어 있습니다.
읽는 법. 가장 가까운 한 마리를 베끼는 KNN(k = 1)이 누수에 가장 크게 속습니다(0.959 → 0.920). 쌍둥이가 바로 옆에 있으니까요. 물고기 단위로 나누는 StratifiedGroupKFold가 "처음 보는 물고기"에 대한 정직한 점수입니다. 원본만 쓴 점수가 더 낮은 것은 누수 때문이 아니라 데이터가 158행뿐이라 학습 재료가 적기 때문입니다.
층화도 함께. 흰송어는 48행이지만 물고기로는 6마리뿐입니다. 물고기 단위로 5겹을 나누면 어떤 조각에는 흰송어가 한 마리도 없을 수 있습니다. StratifiedGroupKFold는 물고기를 묶어 두면서도 종의 비율을 최대한 고르게 맞춥니다.
import pandas as pd, numpy as np, warnings; warnings.filterwarnings("ignore")
# 데이터 불러오기와 정리: 무게 0인 행과 완전히 같은 행을 뺀다
df = pd.read_csv("fish_market_x10.csv")
df = df[df["weight_g"] > 0].drop_duplicates().reset_index(drop=True)
FEATS = ["weight_g", "length1_cm", "length2_cm", "length3_cm", "height_cm", "width_cm"]
X, y, groups = df[FEATS], df["species"], df["seed_id"] # seed_id: 같은 물고기에서 만든 복제본 묶음
# 1장 · 물고기 데이터로 보는 누수: 같은 물고기의 복제본이 학습과 시험에 나뉘면?
from sklearn.model_selection import StratifiedKFold, StratifiedGroupKFold, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
print(df.groupby("species").agg(행=("seed_id", "size"), 물고기=("seed_id", "nunique")).sort_values("행", ascending=False).T)
models = {"KNN (k=1)": make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=1)),
"로지스틱 회귀": make_pipeline(StandardScaler(), LogisticRegression(max_iter=5000)),
"의사결정나무": DecisionTreeClassifier(random_state=0)}
plain = StratifiedKFold(5, shuffle=True, random_state=0) # 행 단위로 섞어 나눔 (누수)
grouped = StratifiedGroupKFold(5, shuffle=True, random_state=0) # 같은 seed_id는 한쪽에만
orig = df[df["source"] == "original"]
print(f"\n{'모델':<10}{'행 단위 분할':>10}{'물고기 단위 분할':>12}{'원본만 (158마리)':>12}")
for name, m in models.items():
a = cross_val_score(m, X, y, cv=plain).mean()
b = cross_val_score(m, X, y, cv=grouped, groups=groups).mean()
c = cross_val_score(m, orig[FEATS], orig["species"], cv=StratifiedKFold(5, shuffle=True, random_state=0)).mean()
print(f"{name:<10}{a:>10.3f}{b:>12.3f}{c:>12.3f}")실행 결과 (scikit-learn 1.8, CPU)
species Perch Bream Roach Pike Smelt Parkki Whitefish 행 560 348 192 164 138 104 48 물고기 56 35 19 17 14 11 6 모델 행 단위 분할 물고기 단위 분할 원본만 (158마리) KNN (k=1) 0.959 0.920 0.810 로지스틱 회귀 0.909 0.899 0.804 의사결정나무 0.884 0.872 0.735
Q1. 소수 클래스가 5%인 데이터에서 "모두 다수 클래스"라고 답하는 모델의 정확도는?
Q2. StandardScaler를 전체 데이터에 먼저 적용한 뒤 train_test_split을 하면?
가중치를 찾아가는 보폭과 방식을 맞춥니다.
같은 모델이라도 가중치를 찾아가는 방식이 나쁘면 성능이 나오지 않습니다. 손실이 줄지 않거나, 줄다가 튀거나, 너무 느리게 줄어드는 경우입니다.
적용 대상 · 이 장은 경사하강법으로 가중치를 찾는 모델에 해당합니다: 로지스틱 회귀, 퍼셉트론, 선형 SVM(SGD 방식), 신경망. KNN과 의사결정나무는 경사하강법을 쓰지 않으므로 해당하지 않습니다.
쉽게 말하면짙은 안개가 낀 산에서 가장 낮은 골짜기를 찾는다고 해 봅시다. 멀리는 안 보이고 발밑의 기울어짐만 느낄 수 있습니다. 경사하강법은 기울어진 아래쪽으로 한 걸음씩 옮기는 방법이고, 학습률은 보폭, 배치는 방향을 정하기 전에 길을 물어보는 사람 수, 에포크는 모든 사람에게 한 번씩 다 물어본 횟수입니다.
momentum=0.0은 순수 경사하강만 비교하려고 일부러 끈 것입니다.solver="adam")입니다.학습 손실(실선)은 에포크가 늘수록 계속 줄지만, 검증 손실(점선)은 어느 순간부터 다시 오릅니다. 그때부터는 새로운 규칙을 배우는 대신 학습 데이터를 외우기 시작한 것입니다(과적합).
조기 종료는 검증 점수가 정해진 횟수(n_iter_no_change) 동안 나아지지 않으면 멈추고, 가장 좋았던 시점의 모델을 씁니다. "에포크를 몇으로 할까"라는 고민을 대신해 줍니다. 그림은 개념을 보여 주는 예시 곡선입니다.
# 2. 학습 과정 조정: 학습률, 에포크, 배치
import math, warnings; warnings.filterwarnings("ignore")
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neural_network import MLPClassifier
X, y = make_moons(n_samples=1000, noise=0.25, random_state=0)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)
sc = StandardScaler().fit(X_tr); X_tr, X_te = sc.transform(X_tr), sc.transform(X_te)
# momentum=0.0: 모멘텀을 꺼서 순수 경사하강법끼리 비교
print("학습률 비교 (배치 32, 에포크 50, 순수 SGD)")
for lr in [0.0001, 0.01, 0.5, 20.0]:
m = MLPClassifier(hidden_layer_sizes=(16, 16), solver="sgd", momentum=0.0,
learning_rate_init=lr, batch_size=32, max_iter=50, random_state=0)
m.fit(X_tr, y_tr)
print(f" η = {lr:<7} 마지막 손실 {m.loss_:.3g} 테스트 정확도 {m.score(X_te, y_te):.3f}")
# 주의: 같은 η, 같은 에포크 비교라서 갱신 횟수가 많은 작은 배치가 유리합니다.
# 실무에서는 배치를 키우면 학습률도 함께 키웁니다.
print("배치 크기 비교 (η = 0.01, 에포크 50)")
for bs in [1, 32, len(X_tr)]:
m = MLPClassifier(hidden_layer_sizes=(16, 16), solver="sgd", momentum=0.0,
learning_rate_init=0.01, batch_size=bs, max_iter=50, random_state=0)
m.fit(X_tr, y_tr)
print(f" 배치 {bs:<4} 한 에포크당 갱신 {math.ceil(len(X_tr) / bs):>3}번 손실 {m.loss_:.3f} 테스트 {m.score(X_te, y_te):.3f}")
print("조기 종료 (검증 점수가 10 에포크 동안 나아지지 않으면 멈춤)")
# solver="adam": 가중치마다 보폭을 자동 조절하고 모멘텀도 쓰는 방식
m = MLPClassifier(hidden_layer_sizes=(64, 64), solver="adam", learning_rate_init=0.01,
max_iter=500, early_stopping=True, validation_fraction=0.15,
n_iter_no_change=10, random_state=0).fit(X_tr, y_tr)
print(f" {m.n_iter_} 에포크에서 멈춤, 테스트 정확도 {m.score(X_te, y_te):.3f}")실행 결과 (scikit-learn 1.8, CPU)
학습률 비교 (배치 32, 에포크 50, 순수 SGD) η = 0.0001 마지막 손실 0.695 테스트 정확도 0.500 η = 0.01 마지막 손실 0.304 테스트 정확도 0.870 η = 0.5 마지막 손실 0.158 테스트 정확도 0.943 η = 20.0 마지막 손실 2.96e+36 테스트 정확도 0.500 배치 크기 비교 (η = 0.01, 에포크 50) 배치 1 한 에포크당 갱신 700번 손실 0.161 테스트 0.940 배치 32 한 에포크당 갱신 22번 손실 0.304 테스트 0.870 배치 700 한 에포크당 갱신 1번 손실 0.629 테스트 0.503 조기 종료 (검증 점수가 10 에포크 동안 나아지지 않으면 멈춤) 26 에포크에서 멈춤, 테스트 정확도 0.940
농어와 로치를 특성 6개로 가르는 로지스틱 회귀를 경사하강법(SGD)으로 학습합니다. 무게의 표준편차는 약 299 g, 두께는 1.6 cm로 단위가 200배 가까이 다릅니다.
| 특성 | η = 0.0001 | η = 0.01 | η = 0.1 |
|---|---|---|---|
| 원래 단위 | 손실 0.635, 시험 0.576 | 손실 15.7, 시험 0.331 | 손실 13.8, 시험 0.457 |
| 표준화 | 손실 0.606, 시험 0.689 | 손실 0.326, 시험 0.874 | 손실 0.165, 시험 0.815 |
읽는 법. 원래 단위에서는 무게 방향의 기울기가 너무 커서, 작은 학습률(0.0001)은 거의 움직이지 못하고 조금만 키워도(0.01) 손실이 15 이상으로 튑니다. 어떤 학습률도 맞지 않는 길쭉한 골짜기입니다. 표준화하면 같은 학습률 0.01로 손실이 매끄럽게 줄어 시험 정확도 0.874에 이릅니다. 학습률을 고르기 전에 단위부터 맞춰야 하는 이유입니다. η = 0.1은 학습 손실이 가장 낮지만 시험 점수는 0.815로 오히려 낮아, 손실만 보고 고르면 안 된다는 것도 보여 줍니다.
import pandas as pd, numpy as np, warnings; warnings.filterwarnings("ignore")
# 데이터 불러오기와 정리: 무게 0인 행과 완전히 같은 행을 뺀다
df = pd.read_csv("fish_market_x10.csv")
df = df[df["weight_g"] > 0].drop_duplicates().reset_index(drop=True)
FEATS = ["weight_g", "length1_cm", "length2_cm", "length3_cm", "height_cm", "width_cm"]
X, y, groups = df[FEATS], df["species"], df["seed_id"] # seed_id: 같은 물고기에서 만든 복제본 묶음
# 2장 · 물고기 데이터로 보는 스케일링과 학습률: 농어 vs 로치, 특성 6개
from sklearn.model_selection import GroupShuffleSplit
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import SGDClassifier
from sklearn.metrics import log_loss
d = df[df["species"].isin(["Perch", "Roach"])]
tr, te = next(GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=0).split(d, groups=d["seed_id"]))
Xtr, Xte = d[FEATS].values[tr], d[FEATS].values[te]
ytr, yte = (d["species"].values[tr] == "Roach"), (d["species"].values[te] == "Roach")
print("특성의 크기 차이:", {f: round(float(d[f].std()), 1) for f in FEATS})
def run(Xa, Xb, eta, epochs=30):
m = SGDClassifier(loss="log_loss", learning_rate="constant", eta0=eta, random_state=0)
losses = []
for _ in range(epochs): # partial_fit 한 번 = 1 에포크
m.partial_fit(Xa, ytr, classes=[False, True])
losses.append(log_loss(ytr, m.predict_proba(Xa), labels=[False, True]))
return losses, m.score(Xb, yte)
sc = StandardScaler().fit(Xtr)
for label, Xa, Xb in [("원래 단위", Xtr, Xte), ("표준화", sc.transform(Xtr), sc.transform(Xte))]:
for eta in [0.0001, 0.01, 0.1]:
L, acc = run(Xa, Xb, eta)
print(f"{label:<6} η={eta:<7} 손실: 1에포크 {L[0]:7.3f} → 30에포크 {L[-1]:7.3f} 시험 정확도 {acc:.3f}")실행 결과 (scikit-learn 1.8, CPU)
특성의 크기 차이: {'weight_g': 299.4, 'length1_cm': 7.5, 'length2_cm': 7.9, 'length3_cm': 8.2, 'height_cm': 2.5, 'width_cm': 1.6}
원래 단위 η=0.0001 손실: 1에포크 0.688 → 30에포크 0.635 시험 정확도 0.576
원래 단위 η=0.01 손실: 1에포크 13.458 → 30에포크 15.737 시험 정확도 0.331
원래 단위 η=0.1 손실: 1에포크 15.717 → 30에포크 13.780 시험 정확도 0.457
표준화 η=0.0001 손실: 1에포크 0.685 → 30에포크 0.606 시험 정확도 0.689
표준화 η=0.01 손실: 1에포크 0.558 → 30에포크 0.326 시험 정확도 0.874
표준화 η=0.1 손실: 1에포크 0.440 → 30에포크 0.165 시험 정확도 0.815Q1. 학습률을 너무 크게 잡으면 흔히 생기는 일은?
Q2. 데이터 1,000개, 배치 크기 50으로 3 에포크를 학습하면 가중치 갱신은 몇 번?
재료를 늘리되, 외우지 않도록 벌점을 겁니다. 입체 지형으로 극소·극대·안장점을 봅니다.
직선 경계 모델도 x₁², x₁x₂ 같은 새 특성을 넣어 주면 곡선 경계를 그릴 수 있습니다(커널 SVC가 차원을 올려 곡선 경계를 만드는 것과 같은 생각). 그런데 특성을 너무 많이 만들면 학습 데이터를 외워 버립니다.
쉽게 말하면특성 공학은 요리 전 재료 손질과 같습니다. 통감자를 채 썰면 볶음 요리가 가능해집니다. 하지만 재료를 수십 가지로 늘리면 요리사가 기출 레시피를 외우듯 학습 데이터에만 맞춘 요리를 만듭니다. 규제는 여행 가방 무게 제한처럼 "짐이 무거울수록 추가 요금"을 매겨 꼭 필요한 짐만 챙기게 합니다. L2(릿지)는 모든 짐을 조금씩 가볍게, L1(라쏘)은 필요 없는 짐을 아예 빼게 합니다.
학습이란 가중치 (w₁, w₂)를 바닥에 두고 그 위에 손실을 높이로 쌓은 지형에서 가장 낮은 곳을 찾는 일입니다. 규제를 켜면 지형 자체가 바뀌어 바닥이 원점 쪽으로 옮겨 갑니다. 네 가지 지형을 골라 공을 굴려 보세요. 드래그하거나 회전 슬라이더로 돌려 볼 수 있습니다.
차수 d와 규제 강도 λ(또는 C)는 데이터로 저절로 정해지지 않는 하이퍼파라미터입니다. 후보 조합을 모두 교차검증으로 비교해 가장 좋은 것을 고르는 것이 그리드 서치입니다. 1장의 교차검증이 여기서 쓰입니다. 시험 세트는 이 선택이 끝난 뒤 한 번만 봅니다.
아래 코드의 마지막 부분이 차수 4가지 × C 5가지 = 20개 조합을 5겹 교차검증으로 비교합니다.
# 3. 특성 생성 → 과적합 → 규제(L2 릿지형, L1 라쏘형)
import warnings; warnings.filterwarnings("ignore")
import numpy as np
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split, GridSearchCV, StratifiedKFold
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import LogisticRegression
X, y = make_moons(n_samples=200, noise=0.3, random_state=1)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.5, random_state=1, stratify=y)
def poly_logit(degree, penalty=None, C=1.0):
clf = (LogisticRegression(C=1e6, max_iter=20000) if penalty is None else
LogisticRegression(penalty=penalty, C=C, solver="liblinear", max_iter=20000))
return make_pipeline(PolynomialFeatures(degree, include_bias=False), StandardScaler(), clf)
print("특성을 늘리면 (규제 없음)")
for d in [1, 3, 10]:
m = poly_logit(d).fit(X_tr, y_tr)
print(f" 차수 {d:>2}: 특성 {m[0].n_output_features_:>2}개 학습 {m.score(X_tr, y_tr):.3f} 테스트 {m.score(X_te, y_te):.3f}")
print("차수 10에 규제를 걸면 (C = 1/λ, 작을수록 강한 규제)")
for C in [10, 0.3]:
for pen in ["l2", "l1"]:
m = poly_logit(10, pen, C=C).fit(X_tr, y_tr)
w = m[-1].coef_.ravel()
print(f" C={C:<4} {pen.upper()}: 학습 {m.score(X_tr, y_tr):.3f} 테스트 {m.score(X_te, y_te):.3f} "
f"0이 된 계수 {np.sum(np.abs(w) < 1e-8)}/{w.size}")
# 규제 강도와 차수를 교차검증으로 고르기 (하이퍼파라미터 탐색)
grid = GridSearchCV(poly_logit(3, "l2"),
{"polynomialfeatures__degree": [1, 3, 5, 10],
"logisticregression__C": [0.01, 0.1, 1, 10, 100]},
cv=StratifiedKFold(5, shuffle=True, random_state=0))
grid.fit(X_tr, y_tr)
print(f"최적 조합 {grid.best_params_} 교차검증 {grid.best_score_:.3f} 테스트 {grid.score(X_te, y_te):.3f}")실행 결과 (scikit-learn 1.8, CPU)
특성을 늘리면 (규제 없음)
차수 1: 특성 2개 학습 0.850 테스트 0.830
차수 3: 특성 9개 학습 0.920 테스트 0.910
차수 10: 특성 65개 학습 1.000 테스트 0.860
차수 10에 규제를 걸면 (C = 1/λ, 작을수록 강한 규제)
C=10 L2: 학습 0.940 테스트 0.920 0이 된 계수 0/65
C=10 L1: 학습 0.940 테스트 0.930 0이 된 계수 55/65
C=0.3 L2: 학습 0.880 테스트 0.850 0이 된 계수 0/65
C=0.3 L1: 학습 0.850 테스트 0.800 0이 된 계수 62/65
최적 조합 {'logisticregression__C': 10, 'polynomialfeatures__degree': 10} 교차검증 0.920 테스트 0.920농어와 로치는 몸길이와 높이만으로는 어떤 모델도 0.8을 넘기 어렵습니다(규칙 기반부터 딥러닝까지 여덟 모델의 시험 정확도 0.26~0.79). 모델 대신 특성을 바꿔 봅니다. "꼬리 비율 = (length3 − length2) ÷ length2", 즉 꼬리지느러미가 몸길이에 비해 얼마나 긴가입니다.
읽는 법. 농어의 꼬리 비율은 0.026~0.081, 로치는 0.093~0.155로 겹치지 않습니다. 원본 158마리만 봐도 같습니다. 그래서 특성 하나를 더한 로지스틱 회귀가 1.000에 이릅니다. 반면 원래 특성 6개를 그대로 다 넣으면 0.917이고, 비율 3개를 더 넣은 9개는 0.901로 오히려 조금 낮아졌습니다. 많이 넣는 것보다 맞는 것을 넣는 것이 중요합니다.
규제와의 관계. 9개 특성에 L1 규제(C = 10)를 걸면 교차검증 1.000이 나오는데, 계수를 보면 length2에 −73, length3에 +73이 붙어 있습니다. 모델이 "length3 − length2", 즉 꼬리 길이를 스스로 만들어 쓴 것입니다. 기본 L2 규제(C = 1)는 이런 큰 계수를 벌점으로 막아 0.901에 머뭅니다. 규제가 과적합을 막아 주지만, 쓸모 있는 신호까지 눌러 버릴 수 있다는 뜻입니다. 사람이 꼬리 비율을 직접 만들어 주면 큰 계수 없이도 같은 결과를 얻어, 모델이 더 단순하고 해석하기 쉬워집니다.
import pandas as pd, numpy as np, warnings; warnings.filterwarnings("ignore")
# 데이터 불러오기와 정리: 무게 0인 행과 완전히 같은 행을 뺀다
df = pd.read_csv("fish_market_x10.csv")
df = df[df["weight_g"] > 0].drop_duplicates().reset_index(drop=True)
FEATS = ["weight_g", "length1_cm", "length2_cm", "length3_cm", "height_cm", "width_cm"]
X, y, groups = df[FEATS], df["species"], df["seed_id"] # seed_id: 같은 물고기에서 만든 복제본 묶음
# 3장 · 물고기 데이터로 보는 특성 공학과 L1 규제: 농어 vs 로치
from sklearn.model_selection import StratifiedGroupKFold, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
d = df[df["species"].isin(["Perch", "Roach"])].copy()
# 새 특성 만들기 (특성 공학): 크기와 상관없는 "체형" 비율
d["height_ratio"] = d["height_cm"] / d["length2_cm"] # 높이 ÷ 길이: 통통한 정도
d["width_ratio"] = d["width_cm"] / d["length2_cm"] # 두께 ÷ 길이
d["plumpness"] = np.cbrt(d["weight_g"]) / d["length2_cm"] # 무게의 세제곱근 ÷ 길이: 살집
d["tail_ratio"] = (d["length3_cm"] - d["length2_cm"]) / d["length2_cm"] # 꼬리 길이 ÷ 몸길이
yy, gg = d["species"], d["seed_id"]
cv = StratifiedGroupKFold(5, shuffle=True, random_state=0)
logit = lambda **kw: make_pipeline(StandardScaler(), LogisticRegression(max_iter=5000, **kw))
sets = {"길이·높이 (2개)": ["length2_cm", "height_cm"],
"길이·높이 + 체형 비율": ["length2_cm", "height_cm", "height_ratio"],
"길이·높이 + 꼬리 비율": ["length2_cm", "height_cm", "tail_ratio"],
"원래 특성 6개": FEATS,
"6개 + 비율 3개 (9개)": FEATS + ["height_ratio", "width_ratio", "plumpness"]}
print(f"기준선 (모두 농어라고 답하기): {(yy == 'Perch').mean():.3f}")
for name, cols in sets.items():
s = cross_val_score(logit(), d[cols], yy, cv=cv, groups=gg)
print(f"{name:<22} {s.mean():.3f} ± {s.std():.3f}")
cols = sets["6개 + 비율 3개 (9개)"]
print("\nL1 규제 강도에 따른 계수 (C가 작을수록 강한 규제)")
for C in [10, 1, 0.1]:
m = logit(penalty="l1", C=C, solver="liblinear").fit(d[cols], yy)
w = m[-1].coef_.ravel()
kept = [f"{c}:{v:+.2f}" for c, v in zip(cols, w) if abs(v) > 1e-6]
acc = cross_val_score(logit(penalty="l1", C=C, solver="liblinear"), d[cols], yy, cv=cv, groups=gg).mean()
print(f"C={C:<4} 남은 특성 {len(kept)}/9 교차검증 {acc:.3f} {', '.join(kept)}")실행 결과 (scikit-learn 1.8, CPU)
기준선 (모두 농어라고 답하기): 0.745 길이·높이 (2개) 0.772 ± 0.033 길이·높이 + 체형 비율 0.835 ± 0.014 길이·높이 + 꼬리 비율 1.000 ± 0.000 원래 특성 6개 0.917 ± 0.017 6개 + 비율 3개 (9개) 0.901 ± 0.029 L1 규제 강도에 따른 계수 (C가 작을수록 강한 규제) C=10 남은 특성 6/9 교차검증 1.000 weight_g:-1.52, length2_cm:-73.16, length3_cm:+73.15, width_cm:-1.76, height_ratio:+1.20, plumpness:-0.19 C=1 남은 특성 6/9 교차검증 0.997 weight_g:-2.04, length2_cm:-32.21, length3_cm:+33.38, width_cm:-1.51, height_ratio:+1.07, plumpness:-0.05 C=0.1 남은 특성 3/9 교차검증 0.877 weight_g:-1.49, height_ratio:+1.76, width_ratio:-0.52
Q1. 학습 정확도 1.00, 시험 정확도 0.86인 모델에 먼저 해 볼 조치는?
Q2. L1 규제가 L2와 달리 "특성 선택" 효과를 내는 이유는?
여러 모델의 판단을 묶어 흔들림과 치우침을 줄입니다.
모델 하나는 오차가 한쪽으로 치우치거나(편향, 식 w·x + b의 편향 항 b와는 다른 뜻), 데이터가 조금만 바뀌어도 판단이 크게 흔들립니다(분산). 여러 모델의 판단을 묶으면 이 약점을 줄일 수 있습니다.
쉽게 말하면보팅은 전공이 다른 전문가들의 투표, 배깅은 같은 과목을 서로 다른 모의고사 세트로 공부한 학생들의 다수결, 랜덤 포레스트는 그 학생들에게 과목도 일부씩만 무작위로 공부하게 해 서로 덜 닮게 만든 것, 부스팅은 앞사람이 틀린 문제를 다음 사람이 집중해서 푸는 오답노트 릴레이입니다.
| 방법 | 잘 맞는 상황 | 주의할 점 |
|---|---|---|
| 보팅 | 성격이 다른 괜찮은 모델이 이미 몇 개 있을 때 | 비슷한 모델끼리 묶으면 효과가 작습니다. 소프트 보팅은 각 모델의 확률이 믿을 만해야 합니다. |
| 배깅 · 랜덤 포레스트 | 깊은 나무처럼 흔들림(분산)이 큰 모델, 표 형태 데이터의 첫 강력한 기준선 | 원래 편향이 큰 단순 모델(예: 로지스틱 회귀)은 배깅해도 크게 좋아지지 않습니다. |
| 부스팅 | 표 형태 데이터에서 최고 성능을 노릴 때 (그래디언트 부스팅, XGBoost, LightGBM) | 차례로 학습해 느리고, 단계 수·학습률을 잘못 잡으면 과적합됩니다. 조기 종료를 함께 씁니다. |
# 4. 여러 알고리즘 혼합: 보팅, 배깅, 랜덤 포레스트, 부스팅
import warnings; warnings.filterwarnings("ignore")
from sklearn.datasets import make_moons
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import (VotingClassifier, BaggingClassifier,
RandomForestClassifier, GradientBoostingClassifier)
X, y = make_moons(n_samples=600, noise=0.35, random_state=2)
cv = StratifiedKFold(5, shuffle=True, random_state=0)
logit = make_pipeline(StandardScaler(), LogisticRegression())
knn = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=15))
tree = DecisionTreeClassifier(random_state=0) # 깊이 제한 없음: 과적합하기 쉬움
models = {
"로지스틱 회귀 (단독)": logit,
"KNN (단독)": knn,
"의사결정나무 (단독)": tree,
"보팅 (위 세 모델, soft)": VotingClassifier([("lr", logit), ("knn", knn),
("tree", DecisionTreeClassifier(max_depth=5, random_state=0))], voting="soft"),
"배깅 (나무 100그루)": BaggingClassifier(DecisionTreeClassifier(), n_estimators=100, random_state=0),
"랜덤 포레스트 (100그루)": RandomForestClassifier(n_estimators=100, random_state=0),
"그래디언트 부스팅 (100단계)": GradientBoostingClassifier(n_estimators=100, max_depth=2, random_state=0),
}
for name, m in models.items():
s = cross_val_score(m, X, y, cv=cv)
print(f"{name:<22} {s.mean():.3f} ± {s.std():.3f}")실행 결과 (scikit-learn 1.8, CPU)
로지스틱 회귀 (단독) 0.832 ± 0.036 KNN (단독) 0.887 ± 0.011 의사결정나무 (단독) 0.842 ± 0.035 보팅 (위 세 모델, soft) 0.883 ± 0.014 배깅 (나무 100그루) 0.870 ± 0.020 랜덤 포레스트 (100그루) 0.878 ± 0.014 그래디언트 부스팅 (100단계) 0.878 ± 0.020
읽는 법. 가장 흔들리는 의사결정나무 1그루(0.842 ± 0.035)가 배깅·랜덤 포레스트·부스팅으로 0.870~0.878로 오르고 표준편차도 줄었습니다. 반면 이 데이터에서는 KNN 단독(0.887)이 앙상블과 비슷합니다. 앙상블은 약하거나 불안정한 모델을 보강하는 도구이지, 언제나 이기는 방법은 아닙니다.
이번에는 7종 전체를 원래 특성 6개로 가릅니다(물고기 단위 5겹 교차검증).
읽는 법. 나무 한 그루(0.872)는 배깅·랜덤 포레스트·부스팅으로 0.899~0.909까지 오릅니다. 앙상블이 불안정한 나무를 보강한 것입니다. 하지만 KNN과 로지스틱 회귀 단독(약 0.900)과의 차이는 가로선(표준편차) 안에 들어가, 이 데이터에서는 "확실히 더 좋다"고 말하기 어렵습니다. 남은 오류의 대부분은 서로 비슷하게 생긴 종(농어·로치·흰송어) 사이에서 나오므로, 3장의 체형 비율과 꼬리 비율을 더해 봤습니다. 특성 8개에서 로지스틱 회귀 단독이 0.980, 랜덤 포레스트가 0.984입니다. 모델을 묶어서 얻은 +0.01~0.04보다, 맞는 특성 두 개를 더해서 얻은 +0.08이 훨씬 큽니다.
import pandas as pd, numpy as np, warnings; warnings.filterwarnings("ignore")
# 데이터 불러오기와 정리: 무게 0인 행과 완전히 같은 행을 뺀다
df = pd.read_csv("fish_market_x10.csv")
df = df[df["weight_g"] > 0].drop_duplicates().reset_index(drop=True)
FEATS = ["weight_g", "length1_cm", "length2_cm", "length3_cm", "height_cm", "width_cm"]
X, y, groups = df[FEATS], df["species"], df["seed_id"] # seed_id: 같은 물고기에서 만든 복제본 묶음
# 4장 · 물고기 7종 분류에서 여러 알고리즘 혼합 (물고기 단위 5겹 교차검증)
from sklearn.model_selection import StratifiedGroupKFold, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import VotingClassifier, BaggingClassifier, RandomForestClassifier, GradientBoostingClassifier
cv = StratifiedGroupKFold(5, shuffle=True, random_state=0)
logit = make_pipeline(StandardScaler(), LogisticRegression(max_iter=5000))
knn = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=5))
models = {
"의사결정나무 (단독)": DecisionTreeClassifier(random_state=0),
"KNN (단독)": knn,
"로지스틱 회귀 (단독)": logit,
"보팅 (위 세 모델, soft)": VotingClassifier([("tree", DecisionTreeClassifier(max_depth=5, random_state=0)), ("knn", knn), ("lr", logit)], voting="soft"),
"배깅 (나무 200그루)": BaggingClassifier(DecisionTreeClassifier(), n_estimators=200, random_state=0),
"랜덤 포레스트 (200그루)": RandomForestClassifier(n_estimators=200, random_state=0),
"그래디언트 부스팅": GradientBoostingClassifier(random_state=0),
}
for name, m in models.items():
s = cross_val_score(m, X, y, cv=cv, groups=groups)
print(f"{name:<18} {s.mean():.3f} ± {s.std():.3f}")
# 모델을 묶는 대신 특성을 더하면? (3장의 체형 비율과 꼬리 비율)
d = df.copy()
d["height_ratio"] = d["height_cm"] / d["length2_cm"]
d["tail_ratio"] = (d["length3_cm"] - d["length2_cm"]) / d["length2_cm"]
cols = FEATS + ["height_ratio", "tail_ratio"]
print("\n특성 8개 (원래 6개 + 비율 2개)")
for name in ["로지스틱 회귀 (단독)", "랜덤 포레스트 (200그루)"]:
s = cross_val_score(models[name], d[cols], d["species"], cv=cv, groups=d["seed_id"])
print(f"{name:<18} {s.mean():.3f} ± {s.std():.3f}")실행 결과 (scikit-learn 1.8, CPU)
의사결정나무 (단독) 0.872 ± 0.017 KNN (단독) 0.900 ± 0.033 로지스틱 회귀 (단독) 0.899 ± 0.036 보팅 (위 세 모델, soft) 0.899 ± 0.031 배깅 (나무 200그루) 0.899 ± 0.026 랜덤 포레스트 (200그루) 0.909 ± 0.034 그래디언트 부스팅 0.903 ± 0.026 특성 8개 (원래 6개 + 비율 2개) 로지스틱 회귀 (단독) 0.980 ± 0.014 랜덤 포레스트 (200그루) 0.984 ± 0.014
Q1. 랜덤 포레스트와 가장 가까운 관계는?
Q2. 부스팅이 다음 모델을 학습할 때 하는 일은?
순서대로 점검하면 대부분의 개선 기회를 놓치지 않습니다.
읽을 때 주의할 점. 움직이는 그림의 데이터와 점수는 원리를 보여 주기 위한 작은 예시이며, 코드 결과표는 scikit-learn 1.8, CPU에서 실행한 값입니다. 실제 데이터에서는 크기와 모양에 따라 효과의 크기가 달라집니다. 3장 입체 지형의 곡면은 설명용 함수로 그렸습니다.
내려받은 코드를 내 데이터로 바꾸거나 다른 모델로 바꾸고 싶을 때, 아래 프롬프트를 복사해 AI 도우미에게 붙여 넣으세요. [ ] 칸을 채우고, 해당하지 않는 항목은 지우면 됩니다.
# 역할 너는 파이썬(pandas, scikit-learn)으로 분류 모델을 가르치는 친절한 데이터 과학 튜터이자 코드 리뷰어다. # 배경 나는 "물고기 분류" 해설 페이지(또는 실험실)에서 코드를 내려받았다. 이 코드를 [데이터 변환 / 모델 변환 / 둘 다] 하고 싶다. - 지금 데이터: [파일 이름], 행 수 [ ] - 기본 열: species(정답), weight_g, length1_cm, length2_cm, length3_cm, height_cm, width_cm, source, seed_id - 내 데이터라면 열 이름과 뜻을 여기에 적는다: [ ] - 실행 환경: [코랩 / 주피터 / 로컬 파이썬 3.x], scikit-learn 버전 [ ](모르면 "모름") - 내 수준: [입문 / 중급 / 실무] # 하고 싶은 일 (해당하는 번호만 남기고 [ ]를 채운다) ## A. 데이터 변환 A1. 다른 데이터로 바꾸기: 파일 [경로], 정답 열 [ ], 특성 열 [ ] A2. 정리: 결측치 [삭제 / 평균 대체 / 중앙값 대체], 이상치 [예: 무게 0인 행 삭제], 완전히 같은 행 [삭제 / 유지] A3. 새 특성 만들기: [예: 체형 비율 = height_cm / length2_cm, 꼬리 비율 = (length3_cm - length2_cm) / length2_cm] A4. 변환: 범주형 [원-핫 / 순서형], 스케일링 [표준화 / 최소-최대 / 없음], 로그 변환할 열 [ ] A5. 데이터 증강: 원본마다 복제본 [n]개, 크기 배율 ~ 정규분포(평균 1, 표준편차 [0.12]), 무게는 배율의 세제곱에 비례, 모양 비율에는 표준편차 [3%]의 잡음. 원본 ID 열(seed_id)을 반드시 만든다. A6. 클래스 고르기·합치기: [예: 농어와 로치 두 종만 / 흰송어와 로치를 한 클래스로] ## B. 모델 변환 B1. 모델 바꾸기: 지금 [ ] → [KNN / 의사결정나무 / 로지스틱 회귀 / 선형 SVM / 커널 SVC / MLP / 랜덤 포레스트 / 그래디언트 부스팅] B2. 하이퍼파라미터: 직접 지정 [ ] 또는 탐색 [GridSearchCV / RandomizedSearchCV], 탐색 범위 [ ] B3. 학습 과정: 학습률 [ ], 에포크 [ ], 배치 크기 [ ], 조기 종료 [사용 / 안 함] B4. 규제: [L1 / L2], 강도 [C 또는 alpha 값] B5. 앙상블: [보팅(모델 목록) / 배깅(기본 모델, 개수) / 부스팅(종류, 단계 수)] B6. 여러 모델 비교표: [비교할 모델 목록] # 반드시 지킬 것 1. 분할: 층화(stratify)로 나눈다. 증강·복제 데이터라면 원본 ID(seed_id) 단위로 나눈다(StratifiedGroupKFold 또는 GroupShuffleSplit). 이유를 주석 한 줄로 적는다. 2. 누수 방지: 스케일링, 특성 선택, 결측치 처리는 Pipeline 안에서 학습 데이터로만 fit한다. 시험 세트는 모든 선택이 끝난 뒤 한 번만 쓴다. 3. 재현성: random_state=0처럼 난수를 고정한다. 4. 기준선: 가장 많은 클래스만 답할 때의 정확도를 함께 출력한다. 5. 출력: 교차검증 정확도(평균 ± 표준편차), 시험 정확도, 혼동행렬, classification_report(클래스별 정밀도·재현율·F1). 6. 원래 코드에서 바뀐 줄에는 "# [변경]" 주석을 단다. 7. 주석과 설명은 한국어로, 입문자도 따라올 수 있게 쓴다. 8. 코랩에서 바로 실행되도록 import를 모두 넣고, 필요하면 파일 올리기 칸(google.colab.files.upload)을 포함한다. # 답변 형식 1. 무엇을 왜 바꿨는지 3~5줄 요약 2. 그대로 실행되는 전체 코드 한 덩어리 (주피터 칸으로 나눌 곳에 "# %%" 표시) 3. 결과를 읽는 법과 주의할 점 (과적합·누수·클래스 불균형 신호) 4. 다음에 해 볼 만한 실험 2~3가지 # 코드 [여기에 내려받은 .py 코드나 노트북 코드를 붙여 넣는다]
프롬프트의 "반드시 지킬 것"은 이 자료에서 다룬 원칙(층화 분할, 원본 단위 분할, 누수 방지, 기준선 비교)을 그대로 옮긴 것입니다. AI가 만든 코드도 이 원칙을 지켰는지 확인하세요.
본문에서 점선 밑줄이 있는 단어를 누르면 같은 비유가 뜹니다. 여기서는 한꺼번에 훑어볼 수 있습니다.
본문에서 점선 밑줄이 있는 단어는 처음 나올 때 눌러서(또는 마우스를 올려서) 같은 비유를 볼 수 있습니다.
비유달리기 기록을 재기 전에 재 두는 "지금 내 기록". 이것이 있어야 좋아졌는지 알 수 있습니다.
뜻가장 단순한 방법(예: 다수 클래스만 답하기, 기본 로지스틱 회귀)으로 먼저 잰 점수.
비유교과서(학습), 모의고사(검증), 수능(시험). 수능 문제는 공부하는 동안 절대 미리 보면 안 됩니다.
뜻가중치를 배우는 데이터, 설정을 고르는 데이터, 마지막에 한 번만 성능을 재는 데이터.
비유여론조사에서 연령대 비율을 실제 인구 비율과 똑같이 맞춰 표본을 뽑는 것.
뜻나눈 뒤에도 각 조각의 클래스 비율이 전체와 같도록 나누는 방법.
비유문제지를 바꿔 가며 모의고사를 여러 번 보고 평균 내기. 한 번 운 좋게 잘 본 점수에 속지 않습니다.
뜻데이터를 K조각으로 나눠 돌아가며 한 조각을 시험용으로 쓰고 점수를 평균 내는 방법.
비유시험 전에 답안지를 슬쩍 본 것. 점수는 높지만 실력이 아닙니다.
뜻시험용 데이터의 정보가 학습 과정에 섞여 들어가 점수가 부풀려지는 실수.
비유100명 중 1명만 걸리는 병의 검사. "모두 음성"이라고만 답해도 99점이 나옵니다.
뜻클래스별 데이터 수가 크게 차이 나는 상태. 정확도가 실력을 가립니다.
비유성적표의 "맞힌 문제/틀린 문제"를 유형별로 나눈 오답 분석표.
뜻실제 클래스와 예측 클래스를 가로세로로 놓고 개수를 센 표.
비유화재경보가 울렸을 때 진짜 불이 났을 확률. 헛경보가 많으면 낮아집니다.
뜻양성이라고 예측한 것 중 실제 양성의 비율. TP / (TP + FP).
비유실제로 난 불 중에서 경보가 울린 비율. 놓친 불이 많으면 낮아집니다.
뜻실제 양성 중 모델이 찾아낸 비율. TP / (TP + FN).
비유정밀도와 재현율 두 과목의 균형 점수. 한 과목만 잘하면 낮게 나옵니다.
뜻정밀도와 재현율의 조화 평균. 2PR / (P + R).
비유다트가 과녁 중심에서 빗나간 거리. 멀수록 나쁩니다.
뜻예측이 정답과 얼마나 다른지를 숫자 하나로 잰 것. 학습은 이 값을 줄이는 과정입니다.
비유짙은 안개 속 산에서 발밑의 기울어짐만 느끼며 한 걸음씩 아래로 내려가 골짜기를 찾는 것.
뜻손실이 줄어드는 방향(기울기의 반대)으로 가중치를 조금씩 옮기는 방법.
비유산을 내려갈 때의 보폭. 너무 크면 골짜기를 건너뛰고, 너무 작으면 해가 질 때까지 못 내려갑니다.
뜻한 걸음에 가중치를 얼마나 크게 옮길지 정하는 값. 기호는 η(에타).
비유문제집 한 권을 처음부터 끝까지 한 번 푸는 것.
뜻학습 데이터 전체를 한 바퀴 다 보는 단위.
비유길을 물을 때 몇 사람에게 물어볼지. 40명 모두에게 물으면 정확하지만 느리고, 한 명에게 물으면 빠르지만 틀릴 수 있습니다.
뜻가중치를 한 번 고칠 때 쓰는 데이터 묶음. 크기를 B로 씁니다.
비유지나가는 사람 한 명에게만 길을 묻고 바로 한 걸음 옮기기. 자주 움직이지만 갈지자로 걷습니다.
뜻배치 크기 1(넓게는 작은 배치)로 매번 가중치를 고치는 경사하강법.
비유내리막을 굴러가던 공의 관성. 한 번 붙은 속도가 작은 요철에 쉽게 멈추지 않습니다.
뜻이전 걸음의 방향을 일부 이어 가는 경사하강법의 보조 장치. 흔들림을 줄이고 수렴을 빠르게 합니다.
비유길마다 보폭을 스스로 조절하는 등산객. 험한 방향은 조심조심, 평탄한 방향은 성큼성큼.
뜻가중치마다 학습률을 자동으로 조절하고 모멘텀도 쓰는 경사하강법의 변형. 신경망의 기본 최적화 방법입니다.
비유모의고사 점수가 더 오르지 않으면 그만 외우고 시험장에 가는 것.
뜻검증 점수가 몇 에포크 동안 나아지지 않으면 학습을 멈추는 방법.
비유주변에서 가장 낮은 골짜기 바닥, 가장 높은 봉우리 꼭대기.
뜻주변보다 값이 가장 작은(큰) 점. 그 자리에서는 기울기가 0입니다.
비유산 중턱의 작은 웅덩이. 내려오다 갇히면 진짜 가장 낮은 계곡을 모릅니다.
뜻주변에서는 가장 낮지만 전체에서 가장 낮지는 않은 점.
비유말안장의 가운데. 앞뒤로는 꼭대기, 좌우로는 바닥입니다.
뜻기울기가 0이지만 극소도 극대도 아닌 점.
비유요리 전 재료 손질. 통감자를 채 썰면 볶음 요리가 가능해지듯, 데이터를 모델이 쓰기 좋게 바꿉니다.
뜻기존 변수로 새 변수(제곱, 곱, 비율 등)를 만들어 모델에 넣는 작업.
비유기출문제 답을 통째로 외운 학생. 기출은 만점, 처음 보는 문제는 못 풉니다.
뜻학습 데이터에는 잘 맞지만 새 데이터에서 성능이 떨어지는 상태.
비유교과서를 대충 훑어본 학생. 기출도 새 문제도 못 풉니다.
뜻모델이 너무 단순해 학습 데이터조차 잘 맞히지 못하는 상태.
비유여행 가방 무게 제한. 무거울수록 추가 요금(벌점)을 물리면 꼭 필요한 짐만 챙기게 됩니다.
뜻가중치가 커질수록 손실에 벌점을 더해 모델을 단순하게 만드는 방법.
비유모든 짐의 무게를 조금씩 줄이기. 버리는 짐은 없습니다.
뜻λ Σ w² 벌점. 계수를 고르게 작게 만들지만 0으로 만들지는 않습니다.
비유필요 없는 짐은 아예 빼기. 가방에 남는 물건 수가 줄어듭니다.
뜻λ Σ |w| 벌점. 일부 계수를 정확히 0으로 만들어 특성을 고르는 효과가 있습니다.
비유요리사가 미리 정하는 불 세기와 조리 시간.
뜻학습 전에 사람이 정하는 설정값. 차수, λ(또는 C), 학습률, 나무 깊이 등.
비유불 세기 3단계 × 조리 시간 4가지 = 12가지를 모두 요리해 보고 맛 평가(교차검증)가 가장 좋은 조합 고르기.
뜻하이퍼파라미터 후보의 모든 조합을 교차검증으로 비교해 가장 좋은 것을 고르는 방법.
비유오케스트라. 악기 하나보다 여럿이 함께 낼 때 소리가 풍성하고 실수가 묻힙니다.
뜻여러 모델의 예측을 묶어 하나의 예측을 만드는 방법.
비유과녁 맞히기. 편향은 화살이 한쪽으로 몰리는 것, 분산은 화살이 사방으로 흩어지는 것.
뜻모델 오차의 두 원인. 단순한 모델은 편향이, 복잡한 모델은 분산이 커지기 쉽습니다. ※ 식 w·x + b의 b를 부르는 "편향(편향 항)"과는 다른 뜻입니다. 여기서는 "예측이 한쪽으로 치우친 정도"를 말합니다.
비유전문가 패널의 투표. 서로 다른 전공의 의견을 모읍니다.
뜻서로 다른 모델들의 예측을 다수결(하드) 또는 확률 평균(소프트)으로 합치는 방법.
비유주머니에서 공을 꺼내 기록하고 다시 넣는 뽑기를 n번. 같은 공이 두 번 나올 수도 있습니다.
뜻원래 데이터에서 중복을 허용해 같은 크기의 표본을 다시 뽑는 방법.
비유같은 과목을 서로 다른 모의고사 세트로 공부한 학생들의 다수결.
뜻부트스트랩 표본마다 같은 종류의 모델을 학습해 평균(다수결)하는 방법. 분산을 줄입니다.
비유그 학생들에게 과목도 일부씩만 무작위로 공부하게 해서 서로 덜 닮게 만들기.
뜻배깅에 "갈림길마다 특성을 무작위로 일부만 보기"를 더한 의사결정나무 앙상블.
비유오답노트 릴레이. 앞사람이 틀린 문제를 다음 사람이 집중해서 공부합니다.
뜻앞 모델이 틀린 데이터에 가중치를 더 주며 모델을 차례로 더해 가는 방법. 편향을 줄입니다.
코드를 직접 실행하려면 물고기 데이터 CSV(fish_market_x10.csv)를 받아 코드와 같은 폴더(또는 Colab)에 올리세요. 강사·튜터라면: 확인 문제의 채점 기준과 과제 예시 답안은 교육자용 정답 페이지 (2)에서 볼 수 있다.
「2 모델을 바꾸기 전에, 성능을 올리는 네 가지 방법」 편을 끝까지 읽었어요. 연구실 식구들이 한마디씩 정리해 드립니다.