평가와 예측
이 챕터에서 배우는 것
학습이 끝난 모델의 성적표를 뽑는 단계입니다.evaluate로 점수를 확인하고,predict로 얻은 확률을 클래스로 바꾼 뒤, 혼동 행렬과classification_report로 "어디에서 틀렸는지"까지 읽어냅니다.
시험 연관성: 실기의 마지막 문항은 거의 언제나 "test 데이터로 성능을 출력하시오"입니다. Ch02에서 어떤 지표를 고를지의 지도를 봤다면, 이 챕터는 그 지표를 실제로 계산하고 해석하는 정본입니다.
7.1 evaluate와 predict — 돌려주는 것이 다릅니다
한 줄 요약 — evaluate는 점수 몇 개를 돌려주고, predict는 샘플 개수만큼의 예측값 배열을 돌려줍니다.
두 함수 모두 학습에 쓰지 않은 평가(테스트) 데이터를 넣어 호출합니다. 다른 점은 결과의 모양입니다.
| 함수 | 필요한 인자 | 반환값 | 언제 쓰나 |
|---|---|---|---|
model.evaluate(X_test_scaled, y_test) |
입력 과 정답 | [loss, metric1, ...] 숫자 리스트 |
전체 성능 점수 한 줄 확인 |
model.predict(X_test_scaled) |
입력만 | shape (샘플 수, 출력 노드 수) 배열 |
샘플별 예측값이 필요할 때 |
evaluate에는 정답 y_test가 반드시 들어갑니다. 오차를 재려면 정답이 있어야 하기 때문입니다. 반대로 predict는 정답 없이 입력만으로 동작합니다. 실제 서비스에서 정답을 모르는 새 데이터를 예측하는 상황과 같습니다.
반환값의 개수는 compile에 넣은 metrics가 결정합니다. metrics=['accuracy']로 컴파일했다면 [loss, accuracy] 두 개가 나오므로 loss, acc = model.evaluate(...)로 받습니다.
학습된 모델에서 두 함수를 나란히 호출해 보는 코드입니다.
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense, Dropout
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
np.random.seed(42)
tf.random.set_seed(42)
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
model = Sequential()
model.add(Input(shape=(X_train_scaled.shape[1],)))
model.add(Dense(64, activation='relu'))
model.add(Dropout(0.3))
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
model.fit(X_train_scaled, y_train, epochs=30, batch_size=32,
validation_split=0.2, verbose=0)
loss, acc = model.evaluate(X_test_scaled, y_test, verbose=0)
print('loss:', round(loss, 4), '/ accuracy:', round(acc, 4))
pred = model.predict(X_test_scaled, verbose=0)
print('predict 결과 shape:', pred.shape)
print('앞 5개 확률:', pred[:5].flatten().round(3))
출력 요지입니다. 학습 난수에 따라 값이 달라집니다 — accuracy는 대략 0.96~0.99 범위에서 나옵니다.
loss: 0.075 / accuracy: 0.9708
predict 결과 shape: (171, 1)
앞 5개 확률: [0.005 0.871 0.952 0.002 0.258]
predict의 결과가 (171, 1)인 점을 눈여겨보십시오. 평가 데이터가 171개이고 출력층이 Dense(1, activation='sigmoid')이므로 샘플 하나당 확률 하나가 세로로 쌓인 2차원 배열입니다.
여기서 0.005는 "이 샘플이 클래스 1일 확률이 0.5%"라는 뜻입니다. 예측된 라벨이 아니라 확률이라는 점이 핵심입니다. 실제로 이 데이터의 정답 앞 5개는 y_test[:5] → [0 1 1 0 0]인데, 위 확률을 0.5 기준으로 판정하면 [0 1 1 0 0]이 되어 다섯 개가 모두 맞습니다. 이 "확률 → 라벨" 변환이 다음 절의 주제입니다.
⚠️ 시험 포인트
evaluate의 반환값 순서는 언제나 loss가 먼저입니다.acc, loss = model.evaluate(...)처럼 순서를 바꿔 받으면 오류 없이 값만 뒤바뀌어, 정확도 0.08 같은 이상한 숫자를 출력하게 됩니다. 또metrics를 지정하지 않고 컴파일했다면 반환값은 loss 숫자 하나뿐이라loss, acc = ...가 오류를 냅니다.
7.2 확률을 클래스로 — 임계값과 argmax
한 줄 요약 — 이진분류는 0.5를 넘는가로, 다중분류는 가장 큰 확률의 위치(argmax)로 라벨을 정합니다.
classification_report와 confusion_matrix는 확률이 아니라 라벨을 받습니다. predict 결과를 그대로 넣으면 계산할 수 없으므로, 그 사이에 변환 한 줄이 반드시 들어갑니다.
| 문제 유형 | predict 결과 shape |
변환 코드 | 결과 |
|---|---|---|---|
| 이진분류 | (N, 1) 확률 |
(pred > 0.5).astype(int).flatten() |
(N,) 0/1 |
| 다중분류 | (N, K) 확률 |
np.argmax(pred, axis=1) |
(N,) 0~K-1 |
| 회귀 | (N, 1) 예측값 |
pred.flatten() (변환 아님, 모양만 정리) |
(N,) 실수 |
(pred > 0.5)는 True/False 배열을 만들고, .astype(int)가 이를 1/0으로 바꿉니다. .flatten()은 (N, 1) 2차원 배열을 (N,) 1차원으로 펴는 작업입니다.
classification_report나 confusion_matrix 같은 sklearn 지표 함수는 (N, 1) 모양을 넣어도 알아서 처리해 줍니다. 문제는 그다음 단계입니다. 예측과 정답을 직접 빼서 잔차를 구하거나 pd.DataFrame({'실제': y_test, '예측': y_pred})로 비교표를 만들려고 하면, (N, 1)과 (N,)이 섞이면서 브로드캐스팅으로 (N, N) 행렬이 생기거나 ValueError가 납니다. 그래서 변환 직후에 한 번 펴 두는 것을 습관으로 삼습니다.
np.argmax(pred, axis=1)의 axis=1은 가로 방향(한 샘플 안의 클래스들) 중 최댓값 위치를 찾으라는 뜻입니다. 이 축 지정이 이 챕터에서 가장 자주 틀리는 지점입니다.
두 변환을 작은 배열로 직접 확인하는 코드입니다.
import numpy as np
# (1) 이진분류 — 출력층 Dense(1, 'sigmoid')의 결과 형태
binary_pred = np.array([[0.83], [0.12], [0.50], [0.91]])
y_pred_bin = (binary_pred > 0.5).astype(int).flatten()
print(y_pred_bin) # [1 0 0 1]
# (2) 다중분류 — 출력층 Dense(3, 'softmax')의 결과 형태 (샘플 4개)
multi_pred = np.array([[0.1, 0.7, 0.2],
[0.6, 0.3, 0.1],
[0.2, 0.2, 0.6],
[0.3, 0.5, 0.2]])
print(multi_pred.sum(axis=1)) # [1. 1. 1. 1.] softmax는 행 합이 1
y_pred_multi = np.argmax(multi_pred, axis=1)
print(y_pred_multi, y_pred_multi.shape) # [1 0 2 1] (4,)
# axis를 잘못 주면 결과 길이부터 달라집니다
wrong = np.argmax(multi_pred, axis=0)
print(wrong, wrong.shape) # [1 0 2] (3,) ← 샘플 수가 아니라 클래스 수
이 예제는 학습이 개입하지 않고 고정된 배열만 다루므로, 환경과 무관하게 언제나 같은 값이 출력됩니다.
세 번째 샘플 [0.2, 0.2, 0.6]에서 가장 큰 값은 세 번째 자리이므로 라벨은 2입니다. 클래스 번호는 0부터 시작한다는 점을 기억하십시오.
axis=0은 세로 방향, 즉 "샘플들 중 몇 번째 샘플이 이 클래스의 확률이 가장 높은가"를 찾습니다. 샘플이 4개인데 결과 길이가 3(클래스 수)이 되어 버렸습니다. 이 상태로 classification_report에 넣으면 정답과 길이가 맞지 않아 오류가 납니다.
⚠️ 시험 포인트
확률을 그대로classification_report(y_test, pred)에 넣으면continuous is not supported계열의 오류가 납니다.predict다음 줄에는 반드시 변환 한 줄이 온다고 외워 두십시오. 다중분류의axis=1도 함께 묶어 기억하면 좋습니다.
7.3 혼동 행렬 — 어디에서 틀렸는지를 봅니다
한 줄 요약 — 혼동 행렬(confusion matrix)은 실제 라벨 × 예측 라벨의 개수를 세어 놓은 표입니다.
정확도는 "몇 개를 맞혔는가" 하나만 알려 줍니다. 혼동 행렬은 틀린 것이 어느 방향으로 틀렸는지까지 보여 줍니다. 이진분류의 네 칸은 다음과 같습니다.
| 약칭 | 이름 | 뜻 |
|---|---|---|
| TN | True Negative | 실제 0을 0으로 맞힘 |
| FP | False Positive | 실제 0인데 1이라고 잘못 예측 |
| FN | False Negative | 실제 1인데 0이라고 놓침 |
| TP | True Positive | 실제 1을 1로 맞힘 |
sklearn의 confusion_matrix(y_true, y_pred)는 행이 실제, 열이 예측이고 라벨을 오름차순으로 배치합니다. 따라서 0/1 이진분류의 결과는 항상 이 배치입니다.
예측 0 예측 1
실제 0 [ TN , FP ]
실제 1 [ FN , TP ]
7.1에서 만든 모델의 혼동 행렬을 뽑고 네 칸을 각각 꺼내는 코드입니다.
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix
pred = model.predict(X_test_scaled, verbose=0)
y_pred = (pred > 0.5).astype(int).flatten()
cm = confusion_matrix(y_test, y_pred)
print(cm)
tn, fp, fn, tp = cm.ravel()
print('TN:', tn, '/ FP:', fp, '/ FN:', fn, '/ TP:', tp)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.title('Confusion Matrix')
plt.show()
출력 요지입니다. 7.1과 같은 실행에서 이어진 결과입니다.
[[ 63 1]
[ 4 103]]
TN: 63 / FP: 1 / FN: 4 / TP: 103
실제 음성 64건 중 1건을 양성으로 잘못 봤고(FP), 실제 양성 107건 중 4건을 놓쳤습니다(FN). 맞힌 것은 63 + 103 = 166건이므로 정확도는 166 / 171 = 0.9708이 되어, 7.1의 evaluate 결과와 정확히 맞아떨어집니다. 정확도 하나로 끝났을 정보가 이렇게 잘못 짚은 1건과 놓친 4건으로 분해됩니다.
이 행렬도 학습 난수에 따라 달라집니다. 다만 이 데이터에서는 TN·FP(윗줄)가 거의 고정되고 FN·TP(아랫줄)만 흔들리는 경향이 있으므로, 여러분의 실행에서는 아랫줄 숫자가 조금 다를 수 있습니다.
cm.ravel()은 2×2 행렬을 왼쪽 위부터 순서대로 편 것이므로 tn, fp, fn, tp 순으로 받습니다. 이 순서는 이진분류(2×2)에서만 성립하며, 다중분류에서는 행렬이 K×K가 되므로 그냥 표 전체를 출력해 클래스별로 읽습니다.
⚠️ 시험 포인트
confusion_matrix(y_pred, y_test)처럼 인자 순서를 바꾸면 행렬이 전치되어 FP와 FN이 뒤바뀝니다. 오류 메시지가 나지 않아 알아채기 어렵습니다. sklearn 평가 함수는 예외 없이 정답이 먼저, 예측이 나중입니다.
7.4 정밀도와 재현율 — 무엇을 더 두려워하는가
한 줄 요약 — 정밀도는 1이라고 말한 것 중 진짜, 재현율은 진짜 1 중 찾아낸 것의 비율입니다.
혼동 행렬의 네 칸에서 두 지표가 바로 나옵니다. 분모가 어디인지만 구분하면 됩니다.
| 지표 | 계산식 | 분모의 방향 | 한 줄 의미 |
|---|---|---|---|
| 정밀도(precision) | TP / (TP + FP) | 혼동 행렬의 예측 1 열 | 1이라고 예측한 것이 얼마나 믿을 만한가 |
| 재현율(recall) | TP / (TP + FN) | 혼동 행렬의 실제 1 행 | 실제 1을 얼마나 빠짐없이 찾아냈는가 |
| f1-score | 두 값의 조화평균 | — | 정밀도와 재현율의 균형 점수 |
앞 절의 숫자(TN 63 / FP 1 / FN 4 / TP 103)를 넣으면 정밀도는 103 / (103 + 1) = 0.990, 재현율은 103 / (103 + 4) = 0.963입니다. 1이라고 예측한 104건은 거의 다 맞혔지만(정밀도 높음), 실제 1인 107건 중 4건을 놓쳤습니다(재현율이 상대적으로 낮음).
두 지표는 대체로 한쪽을 올리면 다른 쪽이 내려갑니다. 의심스러운 것까지 모두 1이라고 예측하면 놓치는 것은 줄지만(재현율↑) 잘못 짚는 것이 늘고(정밀도↓), 확실한 것만 1이라고 예측하면 반대가 됩니다. 그래서 어느 쪽을 중시할지는 문제의 성격이 정합니다.
- 스팸 필터 — 정상 메일이 스팸함으로 가면 사용자가 중요한 메일을 잃습니다. FP가 더 치명적이므로 정밀도를 중시합니다.
- 질병 검진 — 환자를 정상으로 판정하면 치료 시기를 놓칩니다. FN이 더 치명적이므로 재현율을 중시합니다.
f1-score는 둘 중 어느 쪽도 특별히 중요하지 않거나, 한 숫자로 비교해야 할 때 씁니다. 조화평균이라 한쪽이 크게 낮으면 f1도 함께 떨어지는 성질이 있어, 두 지표의 균형을 판단하기 좋습니다.
📎 임계값을 0.5에서 옮기면
재현율을 더 올리고 싶다면 임계값을 0.3처럼 낮춰(pred > 0.3)으로 판정하면 됩니다. 1이라고 판정하는 범위가 넓어지므로 놓치는 것이 줄고 잘못 짚는 것이 늘어납니다. 다만 실기 문항에서 별도 지시가 없으면 기본값 0.5를 쓰십시오. 임의로 바꾸면 채점 기준과 어긋날 수 있습니다.
7.5 classification_report — 읽는 순서가 있습니다
한 줄 요약 — classification_report는 클래스별 정밀도·재현율·f1과 표본 수(support)를 한 표로 보여 줍니다.
from sklearn.metrics import classification_report
print(classification_report(y_test, y_pred))
출력 요지입니다. 7.3의 혼동 행렬 [[63, 1], [4, 103]]과 같은 실행에서 이어진 결과이므로, 표의 모든 숫자를 그 네 칸에서 직접 계산해 낼 수 있습니다(학습 난수에 따라 달라집니다).
precision recall f1-score support
0 0.94 0.98 0.96 64
1 0.99 0.96 0.98 107
accuracy 0.97 171
macro avg 0.97 0.97 0.97 171
weighted avg 0.97 0.97 0.97 171
클래스 1의 precision 0.99는 7.4에서 손으로 계산한 103 / 104 = 0.990, recall 0.96은 103 / 107 = 0.963과 같은 값입니다. 클래스 0의 precision은 63 / (63 + 4) = 0.940, recall은 63 / 64 = 0.984입니다. 리포트의 숫자는 전부 혼동 행렬에서 나옵니다.
읽는 순서는 세 단계입니다.
① 클래스별 행부터 봅니다. 맨 위의 0, 1은 클래스 번호입니다. 전체 정확도가 높아도 특정 클래스의 recall만 0.4처럼 낮게 주저앉는 경우가 흔한데, 그 사실은 이 행에서만 드러납니다.
② support로 불균형을 확인합니다. support는 그 클래스의 실제 표본 개수입니다. 여기서는 64 대 107이라 큰 문제가 없지만, 90 대 10처럼 치우쳐 있다면 accuracy는 신뢰할 수 없는 숫자가 됩니다.
③ macro avg와 weighted avg의 차이를 봅니다. macro avg는 클래스별 점수를 표본 수와 무관하게 단순 평균한 값이고, weighted avg는 support로 가중 평균한 값입니다. 두 값이 크게 벌어져 있다면 "표본이 적은 클래스의 성능이 나쁘다"는 신호입니다. 위 표에서는 둘 다 0.97로 붙어 있는데, 두 클래스의 성능이 고르다는 뜻입니다. 벌어지는 경우를 바로 아래에서 봅니다.
정확도의 함정을 숫자로 확인하는 코드입니다. 90:10으로 불균형한 데이터에서 모델이 전부 0으로만 찍었다고 가정합니다.
import numpy as np
from sklearn.metrics import accuracy_score, classification_report
y_true = np.array([0] * 90 + [1] * 10)
y_all_zero = np.zeros(100, dtype=int) # 무조건 0이라고 예측
print('accuracy:', accuracy_score(y_true, y_all_zero))
print(classification_report(y_true, y_all_zero, zero_division=0))
출력입니다. 이 예제는 학습이 개입하지 않으므로 환경과 무관하게 항상 아래와 똑같이 나옵니다.
accuracy: 0.9
precision recall f1-score support
0 0.90 1.00 0.95 90
1 0.00 0.00 0.00 10
accuracy 0.90 100
macro avg 0.45 0.50 0.47 100
weighted avg 0.81 0.90 0.85 100
정확도는 0.90이지만 클래스 1의 recall은 0.00입니다. 찾아야 할 대상을 하나도 찾지 못한 모델인데도 정확도만 보면 훌륭해 보입니다. macro avg가 0.47로 주저앉은 것이 이 상태를 드러냅니다. 이탈 고객 예측이나 불량 검출처럼 소수 클래스가 중요한 문제에서는 반드시 이 표를 함께 출력하십시오.
zero_division=0은 분모가 0이 되는 클래스에서 경고 대신 0을 출력하도록 하는 인자입니다. 없어도 계산은 되지만 경고가 함께 출력됩니다.
⚠️ 시험 포인트
"성능을 출력하시오"라는 지시에evaluate결과만 한 줄 찍고 끝내지 마십시오.evaluate(loss·accuracy) +classification_report+confusion_matrix세 가지를 함께 출력하는 것이 안전한 답안입니다. 채점자는 출력된 결과로 판단하므로,print()로 화면에 남기지 않으면 계산했어도 인정받기 어렵습니다.
7.6 회귀 지표 — mse, mae, r2
한 줄 요약 — mse는 큰 오차에 민감한 점수, mae는 평균적인 오차 크기, r2는 0~1 사이의 설명력입니다.
회귀에는 "맞혔다/틀렸다"가 없습니다. 예측값과 실제값의 차이(잔차)를 어떻게 요약하는가가 지표의 차이를 만듭니다.
| 지표 | sklearn 함수 | 초점 | 좋은 방향 |
|---|---|---|---|
| mse | mean_squared_error |
잔차를 제곱해 평균 — 큰 오차에 크게 반응 | 작을수록 좋음 |
| mae | mean_absolute_error |
잔차의 절댓값 평균 — 평균 오차 크기 | 작을수록 좋음 |
| r2 | r2_score |
타깃 분산 중 모델이 설명한 비율 | 1에 가까울수록 좋음 |
mse는 제곱을 하므로 단위가 타깃의 제곱입니다. 집값을 만원 단위로 예측했다면 mse의 단위는 "만원 제곱"이라 크기를 직관적으로 읽기 어렵습니다. 그래서 해석할 때는 mae를 함께 봅니다. mae가 4.2라면 "평균적으로 4.2만큼 빗나간다"로 바로 읽힙니다. mse에 제곱근을 씌운 rmse를 쓰면 단위가 원래대로 돌아옵니다.
r2는 크기 단위가 없어 서로 다른 문제끼리도 비교할 수 있습니다. 0이면 "평균값으로 찍는 것과 다를 바 없음", 1이면 완벽, 음수도 나올 수 있으며 평균보다 못하다는 뜻입니다.
회귀 모델을 만들고 세 지표를 계산하는 코드입니다.
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense
from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
np.random.seed(42)
tf.random.set_seed(42)
Xr, yr = load_diabetes(return_X_y=True)
Xr_train, Xr_test, yr_train, yr_test = train_test_split(
Xr, yr, test_size=0.3, random_state=42
)
sc = StandardScaler()
Xr_train_scaled = sc.fit_transform(Xr_train)
Xr_test_scaled = sc.transform(Xr_test)
reg = Sequential()
reg.add(Input(shape=(Xr_train_scaled.shape[1],)))
reg.add(Dense(64, activation='relu'))
reg.add(Dense(32, activation='relu'))
reg.add(Dense(1)) # 활성화 없음
reg.compile(optimizer='adam', loss='mse', metrics=['mae'])
reg.fit(Xr_train_scaled, yr_train, epochs=100, batch_size=32,
validation_split=0.2, verbose=0)
loss, mae_from_keras = reg.evaluate(Xr_test_scaled, yr_test, verbose=0)
print('keras mse:', round(loss, 2), '/ keras mae:', round(mae_from_keras, 2))
y_pred_reg = reg.predict(Xr_test_scaled, verbose=0).flatten() # (N,1) → (N,)
print('예측 shape:', y_pred_reg.shape, '/ 정답 shape:', yr_test.shape)
mse = mean_squared_error(yr_test, y_pred_reg)
mae = mean_absolute_error(yr_test, y_pred_reg)
rmse = np.sqrt(mse)
r2 = r2_score(yr_test, y_pred_reg)
print('mse:', round(mse, 2))
print('rmse:', round(rmse, 2))
print('mae:', round(mae, 2))
print('r2:', round(r2, 4))
출력 요지입니다. 학습 난수에 따라 값이 달라집니다 — 반복 실행하면 mse는 3,300~3,450, r2는 0.36~0.39 부근에서 움직입니다.
keras mse: 3348.74 / keras mae: 45.34
예측 shape: (133,) / 정답 shape: (133,)
mse: 3348.74
rmse: 57.87
mae: 45.34
r2: 0.3797
두 가지를 확인하십시오. 첫째, evaluate의 loss와 sklearn의 mean_squared_error 값이 같습니다. 회귀에서 loss='mse'로 컴파일하면 손실 함수 자체가 평가지표이기 때문입니다. 둘째, 회귀 문제에서는 metrics=['mae']를 함께 지정해 두면 학습 중에도 해석하기 쉬운 오차 크기를 볼 수 있습니다.
r2가 0.38이라는 것은 타깃의 변동 중 38% 정도만 설명했다는 뜻입니다. 낮아 보이지만 이 데이터는 원래 예측이 어려운 편이므로, 절대 기준보다 베이스라인 대비 개선 여부로 판단합니다. mae 45.34는 "평균적으로 45 정도 빗나간다"로 읽고, mse 3348.74는 단위가 제곱이라 크기를 직관적으로 읽기 어려우므로 rmse 57.87로 바꿔 보면 타깃과 같은 단위로 비교할 수 있습니다.
⚠️ 시험 포인트
predict결과는(N, 1)인데y_test는(N,)입니다.mean_squared_error같은 sklearn 지표 함수는 이 상태로도 값을 맞게 내주지만, 그다음이 문제입니다. 잔차를y_test - y_pred로 직접 계산하면 브로드캐스팅이 일어나(N, N)행렬이 되고,pd.DataFrame({'실제': y_test, '예측': y_pred})로 비교표를 만들면ValueError가 납니다. 회귀에서는predict(...).flatten()을 습관으로 붙이십시오..ravel(),.reshape(-1)도 같은 결과를 냅니다.
7.7 학습 곡선으로 평가 결과를 뒷받침합니다
평가 점수 한 줄만으로는 "왜 이 점수가 나왔는가"를 설명할 수 없습니다. fit이 돌려준 history로 학습 곡선(learning curve)을 함께 그리면, 점수가 낮은 원인이 학습 부족인지 과적합인지 한눈에 드러납니다. 곡선의 구성 요소는 Ch05의 ch05_history-curve, 과적합 판별 기준은 Ch06의 ch06_overfit-vs-good에서 다뤘습니다. 여기서는 평가 문항에서 그대로 재사용할 플롯 함수만 정리합니다.
loss와 accuracy를 2패널로 그리는 재사용 함수입니다.
import matplotlib.pyplot as plt
def plot_history(history, metric='accuracy'):
h = history.history
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.plot(h['loss'], label='train loss')
plt.plot(h['val_loss'], label='val loss')
plt.xlabel('epoch'); plt.ylabel('loss'); plt.legend(); plt.title('Loss')
if metric in h:
plt.subplot(1, 2, 2)
plt.plot(h[metric], label='train ' + metric)
plt.plot(h['val_' + metric], label='val ' + metric)
plt.xlabel('epoch'); plt.ylabel(metric); plt.legend(); plt.title(metric)
plt.tight_layout()
plt.show()
그리고 곡선을 그릴 모델은 반드시 새로 구성한 뒤 학습시킵니다.
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense, Dropout
# 7.1의 model을 그대로 다시 fit 하면 "이어 학습"이 되어
# 곡선이 처음부터 평탄하게 나옵니다. 새 모델을 만들어야 합니다.
curve_model = Sequential()
curve_model.add(Input(shape=(X_train_scaled.shape[1],)))
curve_model.add(Dense(64, activation='relu'))
curve_model.add(Dropout(0.3))
curve_model.add(Dense(32, activation='relu'))
curve_model.add(Dense(1, activation='sigmoid'))
curve_model.compile(optimizer='adam', loss='binary_crossentropy',
metrics=['accuracy'])
history = curve_model.fit(X_train_scaled, y_train, epochs=30, batch_size=32,
validation_split=0.2, verbose=0)
print(history.history.keys())
plot_history(history, metric='accuracy')
출력 요지: dict_keys(['accuracy', 'loss', 'val_accuracy', 'val_loss'])와 2패널 그래프가 나옵니다.
⚠️ 시험 포인트
Keras의fit은 가중치를 초기화하지 않습니다. 이미 학습을 마친 모델에fit을 한 번 더 부르면 처음부터 다시 배우는 것이 아니라 이어서 학습합니다. 그래서 앞의model로 곡선을 다시 뽑으면 이미 수렴한 구간만 그려져 아무것도 읽어낼 수 없습니다. 학습을 다시 하고 싶다면 모델을 새로 구성하십시오. 시험장에서 셀을 여러 번 실행하다 성능이 슬금슬금 달라지는 원인도 대개 이것입니다.
print(history.history.keys())로 먼저 키 이름을 확인하는 습관이 중요합니다. 회귀 모델을 metrics=['mae']로 컴파일했다면 키는 accuracy가 아니라 mae, val_mae이므로 plot_history(history, metric='mae')로 호출해야 합니다. 키 이름을 확인하지 않고 h['accuracy']를 직접 쓰면 KeyError가 납니다.
시험장 표준 코드
전처리를 마친 데이터에서 평가·예측 전 과정을 출력까지 마무리하는 완결 코드입니다. 이진분류 기준이며, 유형이 바뀌는 지점을 주석으로 표시했습니다.
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
import seaborn as sns
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense, Dropout
from tensorflow.keras.callbacks import EarlyStopping
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, confusion_matrix
np.random.seed(42)
tf.random.set_seed(42)
# ===== 1) 데이터 준비 (Ch02) =====
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# ===== 2) 구성·컴파일·학습 (Ch04~Ch06) =====
model = Sequential()
model.add(Input(shape=(X_train_scaled.shape[1],)))
model.add(Dense(64, activation='relu'))
model.add(Dropout(0.3))
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid')) # 다중: Dense(K,'softmax') / 회귀: Dense(1)
model.compile(optimizer='adam',
loss='binary_crossentropy', # 다중: sparse_categorical_crossentropy / 회귀: mse
metrics=['accuracy']) # 회귀: ['mae']
es = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True)
history = model.fit(X_train_scaled, y_train, epochs=100, batch_size=32,
validation_split=0.2, callbacks=[es], verbose=0)
# ===== 3) 평가 (이 챕터) =====
loss, acc = model.evaluate(X_test_scaled, y_test, verbose=0)
print('test loss:', round(loss, 4))
print('test accuracy:', round(acc, 4))
# ===== 4) 예측 → 클래스 변환 =====
pred = model.predict(X_test_scaled, verbose=0)
y_pred = (pred > 0.5).astype(int).flatten() # 다중: np.argmax(pred, axis=1)
# ===== 5) 지표 출력 =====
print(classification_report(y_test, y_pred))
cm = confusion_matrix(y_test, y_pred)
print(cm)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.xlabel('Predicted'); plt.ylabel('Actual'); plt.show()
# ===== 6) 학습 곡선 =====
plt.plot(history.history['loss'], label='train loss')
plt.plot(history.history['val_loss'], label='val loss')
plt.xlabel('epoch'); plt.ylabel('loss'); plt.legend(); plt.show()
회귀 문제라면 4)와 5) 블록을 통째로 아래로 교체하고, 여기에 더해 3) 블록의 acc를 mae로 바꿉니다. 회귀는 metrics=['mae']로 컴파일하므로 evaluate의 두 번째 반환값이 정확도가 아니라 mae이기 때문입니다.
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
# ===== 3) 평가 — 이 두 줄로 교체 =====
loss, mae = model.evaluate(X_test_scaled, y_test, verbose=0)
print('test mse:', round(loss, 4))
print('test mae:', round(mae, 4))
# ===== 4)~5) 예측과 지표 — 이 블록으로 교체 =====
y_pred = model.predict(X_test_scaled, verbose=0).flatten() # 클래스 변환 없음, 모양만 정리
print('mse:', mean_squared_error(y_test, y_pred))
print('mae:', mean_absolute_error(y_test, y_pred))
print('r2 :', r2_score(y_test, y_pred))
6) 학습 곡선 블록은 그대로 두어도 되지만, plot_history를 쓴다면 metric='mae'로 호출하십시오.
핵심 요약
| 항목 | 기억할 것 |
|---|---|
evaluate |
loss, acc = model.evaluate(X_test_scaled, y_test) — loss가 먼저, 정답 필요 |
predict |
확률 배열 (N, 출력 노드 수) 반환, 정답 불필요 |
| 이진 변환 | (pred > 0.5).astype(int).flatten() |
| 다중 변환 | np.argmax(pred, axis=1) — 축은 1 |
| 회귀 정리 | predict(...).flatten()으로 (N,) 맞추기 |
| 혼동 행렬 | confusion_matrix(y_test, y_pred) — 정답이 먼저, 배치는 [[TN, FP], [FN, TP]] |
| 정밀도/재현율 | TP/(TP+FP) / TP/(TP+FN) — 스팸은 정밀도, 검진은 재현율 |
| 리포트 | 클래스별 행 → support로 불균형 확인 → macro avg와 weighted avg 비교 |
| 회귀 지표 | mse(큰 오차 민감) / mae(평균 오차 크기) / r2(1에 가까울수록 좋음, 음수 가능) |
평가 문항 3종 세트: evaluate 점수 + classification_report + confusion_matrix를 모두 print()로 남깁니다.
Ch07 확인 문제 — 평가와 예측
본 문제는 공개된 출제 기준에 따른 유형 연습 문제입니다. 기출 원문이 아닙니다.
이 챕터의 범위는 학습이 끝난 뒤의 평가·예측입니다.
evaluate·predict, 확률→클래스 변환,
classification_report·confusion_matrix, 회귀 지표(mse·mae·r2)만으로 모든 문항이 풀립니다.총 5문항 (객관식 2 / 코드 빈칸 2 / 오류 찾기 1), 권장 소요 시간 20분.
공통 import는 다음과 같다고 가정합니다.
python import numpy as np import pandas as pd from sklearn.metrics import (classification_report, confusion_matrix, mean_squared_error, mean_absolute_error, r2_score)
문제
Q1. (객관식 · 기본) evaluate와 predict가 돌려주는 것
클래스가 3개인 다중분류 모델을 metrics=['accuracy']로 컴파일해 학습까지 마쳤습니다.
평가(테스트) 데이터 X_test의 샘플 수는 200개입니다.
다음 설명 중 옳은 것은?
model.evaluate(X_test)처럼 정답 없이 호출해도 loss와 accuracy가 계산된다.model.evaluate(X_test, y_test)의 반환값은 shape(200, 3)인 확률 배열이다.loss, acc = model.evaluate(X_test, y_test)로 받을 수 있고,model.predict(X_test)의 결과는 shape(200, 3)이다.model.predict(X_test)는 예측 라벨(200,)을 돌려주므로 그대로classification_report에 넣을 수 있다.evaluate는 지표를 알파벳 순으로 돌려주므로acc, loss = model.evaluate(X_test, y_test)로 받아야 한다.
Q2. (객관식 · 응용) 인자 순서를 바꿔 쓴 혼동 행렬
이진분류 평가에서 confusion_matrix(y_test, y_pred)의 정상 출력이 다음과 같습니다.
[[80 20]
[10 90]]
그런데 수험생 A는 실수로 아래처럼 인자 순서를 바꿔 쓴 뒤, 그 행렬로 클래스 1의 재현율(recall) 을 계산했습니다.
cm = confusion_matrix(y_pred, y_test) # 순서가 뒤바뀜
tn, fp, fn, tp = cm.ravel()
recall = tp / (tp + fn)
print(round(recall, 4))
클래스 1의 올바른 재현율과 A가 출력한 값은 각각 얼마입니까?
- 올바른 값 0.9000 / A의 값 0.9000
- 올바른 값 0.9000 / A의 값 0.8182
- 올바른 값 0.8182 / A의 값 0.9000
- 올바른 값 0.8182 / A의 값 0.8182
- 인자 순서가 잘못되었으므로
ValueError가 발생해 값이 출력되지 않는다
Q3. (코드 빈칸 · 기본) 이진분류 평가 3종 세트
유방암 데이터로 학습을 마친 이진분류 모델 model이 있습니다.
출력층은 Dense(1, activation='sigmoid'), 컴파일은 loss='binary_crossentropy', metrics=['accuracy']입니다.
X_test는 171개 샘플, y_test는 0/1 정수 라벨입니다.
"test 데이터의 성능을 출력하시오" 라는 지시에 맞게 (가)~(라)를 채우십시오.
import numpy as np
from sklearn.metrics import classification_report, confusion_matrix
# (가) evaluate 결과를 두 변수로 받아 출력합니다
______(가)______ = model.evaluate(X_test, y_test, verbose=0)
print('test loss :', round(loss, 4))
print('test accuracy:', round(acc, 4))
# 확률 예측
pred = model.predict(X_test, verbose=0)
print('pred shape:', pred.shape)
# (나) 확률을 0/1 라벨로 바꿉니다 (임계값 0.5, 1차원으로 정리)
y_pred = ______(나)______
print('y_pred shape:', y_pred.shape)
# (다) 클래스별 정밀도·재현율·f1 표 출력
print(______(다)______)
# (라) 혼동 행렬 출력 후 네 칸을 각각 꺼냅니다
cm = ______(라)______
print(cm)
tn, fp, fn, tp = cm.ravel()
print('TN:', tn, '/ FP:', fp, '/ FN:', fn, '/ TP:', tp)
추가로, pred.shape와 y_pred.shape가 각각 무엇으로 출력되는지 쓰십시오.
Q4. (코드 빈칸 · 기본) 세 유형의 예측값 변환
세 개의 서로 다른 모델이 각각 predict를 마쳤고, 그 결과가 아래 배열로 주어졌습니다.
문제 유형에 맞는 변환 한 줄씩을 (가)~(다)에, 회귀 지표 계산을 (라)에 채우십시오.
import numpy as np
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
# ① 이진분류 모델의 predict 결과 — 출력층 Dense(1, 'sigmoid')
bin_pred = np.array([[0.83], [0.12], [0.50], [0.91], [0.46]])
# ② 다중분류 모델의 predict 결과 — 출력층 Dense(3, 'softmax')
multi_pred = np.array([[0.1, 0.7, 0.2],
[0.6, 0.3, 0.1],
[0.2, 0.2, 0.6],
[0.3, 0.5, 0.2],
[0.8, 0.1, 0.1]])
# ③ 회귀 모델의 predict 결과 — 출력층 Dense(1), 활성화 없음
reg_pred = np.array([[151.2], [74.8], [140.5], [205.9], [134.7]])
y_reg_true = np.array([155.0, 70.0, 145.0, 200.0, 130.0])
y_bin = ______(가)______
y_mul = ______(나)______
y_reg = ______(다)______
print('(가) 이진:', y_bin, y_bin.shape)
print('(나) 다중:', y_mul, y_mul.shape)
print('(다) 회귀:', y_reg, y_reg.shape)
# (라) 회귀 지표 세 가지를 계산합니다
print('mse:', round(______(라-1)______, 3))
print('mae:', round(______(라-2)______, 3))
print('r2 :', round(______(라-3)______, 4))
추가로, (나)에서 축을 axis=0으로 잘못 지정하면 결과 shape가 무엇이 되고 그다음 어떤 문제가 생기는지 쓰십시오.
Q5. (오류 찾기 · 응용) 평가 코드의 두 가지 실수
아래는 학습을 마친 이진분류 모델의 평가 코드입니다.
잘못된 곳이 두 군데 있습니다. 각각을 지적하고 수정한 전체 코드를 쓰십시오.
두 오류 중 하나는 실행이 중단되고, 다른 하나는 오류 메시지 없이 결과만 틀어진다는 점에 유의하십시오.
import numpy as np
from sklearn.metrics import classification_report, confusion_matrix
loss, acc = model.evaluate(X_test, y_test, verbose=0)
print('test accuracy:', round(acc, 4))
pred = model.predict(X_test, verbose=0)
y_pred = (pred > 0.5).astype(int).flatten()
print(classification_report(y_test, pred))
cm = confusion_matrix(y_pred, y_test)
print(cm)
해답 및 해설 보기
Q1 해답
정답: 3번
정답 코드 (실증 예제 — 이진분류 모델로 반환 형태를 확인)
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense, Dropout
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
np.random.seed(42)
tf.random.set_seed(42)
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
model = Sequential()
model.add(Input(shape=(X_train_scaled.shape[1],)))
model.add(Dense(64, activation='relu'))
model.add(Dropout(0.3))
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
model.fit(X_train_scaled, y_train, epochs=30, batch_size=32,
validation_split=0.2, verbose=0)
res = model.evaluate(X_test_scaled, y_test, verbose=0)
print('evaluate 반환 타입:', type(res).__name__, '/ 길이:', len(res))
loss, acc = res
print('loss:', round(loss, 4), '/ accuracy:', round(acc, 4))
pred = model.predict(X_test_scaled, verbose=0)
print('predict shape:', pred.shape, '/ dtype:', pred.dtype)
print('앞 3개:', pred[:3].flatten().round(3))
evaluate 반환 타입: list / 길이: 2
loss: 0.068 / accuracy: 0.9766
predict shape: (171, 1) / dtype: float32
앞 3개: [0.001 0.957 0.891]
출력의 loss·accuracy 값은 실행할 때마다 조금씩 달라집니다(검증 시 accuracy 0.97~0.98 범위).
반환 타입(길이 2인 리스트)과predict결과의 shape 가 이 문항에서 확인할 대상입니다.
왜 이렇게 푸는가
두 함수는 목적이 다르므로 필요한 인자와 결과의 모양이 모두 다릅니다.
evaluate |
predict |
|
|---|---|---|
| 인자 | 입력 + 정답 | 입력만 |
| 반환 | [loss, metric1, ...] 숫자 리스트 |
(샘플 수, 출력 노드 수) 배열 |
| 이 문항 | 길이 2 → loss, acc = ... |
(200, 3) |
evaluate는 오차를 재는 함수이므로 정답이 없으면 계산 자체가 성립하지 않습니다. 반대로 predict는 정답 없이 동작합니다(실제 서비스에서 정답을 모르는 새 데이터를 예측하는 상황과 같습니다).
predict의 shape (200, 3)은 "샘플 200개 × 클래스 3개의 확률" 이라는 뜻입니다. 출력층이 Dense(3, 'softmax')이므로 열이 3개가 되고, 각 행의 합은 1입니다.
자주 틀리는 지점
- 5번 — 반환 순서를 헷갈리는 것.
evaluate의 반환은 언제나 loss가 먼저이고, 그 뒤로compile의metrics에 적은 순서대로 붙습니다.acc, loss = ...로 받으면 오류 없이 값만 뒤바뀌어 "정확도 0.06" 같은 이상한 숫자를 출력하게 됩니다. - 4번 —
predict가 라벨을 준다고 오해하는 것.predict가 주는 것은 확률입니다.classification_report에 그대로 넣으면ValueError가 납니다(Q5 참조). metrics를 지정하지 않고 컴파일했다면 반환값은 loss 숫자 하나뿐이라loss, acc = ...가 언패킹 오류를 냅니다. 받기 전에print(model.evaluate(...))로 길이를 한 번 확인하면 안전합니다.
Q2 해답
정답: 2번 (올바른 값 0.9000 / A의 값 0.8182)
정답 코드 (실증 예제)
import numpy as np
from sklearn.metrics import confusion_matrix
y_test = np.array([0] * 100 + [1] * 100)
y_pred = np.array([0] * 80 + [1] * 20 + [0] * 10 + [1] * 90)
cm_ok = confusion_matrix(y_test, y_pred) # 정답이 먼저 (올바름)
cm_sw = confusion_matrix(y_pred, y_test) # 순서가 뒤바뀜
print('정상 :\n', cm_ok)
print('뒤바뀜:\n', cm_sw)
tn, fp, fn, tp = cm_ok.ravel()
print('정상 ravel -> TN,FP,FN,TP =', tn, fp, fn, tp)
print('정상 recall(1) =', round(tp / (tp + fn), 4))
print('정상 precision(1) =', round(tp / (tp + fp), 4))
tn2, fp2, fn2, tp2 = cm_sw.ravel()
print('뒤바뀜 ravel -> TN,FP,FN,TP =', tn2, fp2, fn2, tp2)
print('뒤바뀜으로 계산한 recall =', round(tp2 / (tp2 + fn2), 4))
print('두 행렬이 전치 관계인가:', np.array_equal(cm_ok, cm_sw.T))
정상 :
[[80 20]
[10 90]]
뒤바뀜:
[[80 10]
[20 90]]
정상 ravel -> TN,FP,FN,TP = 80 20 10 90
정상 recall(1) = 0.9
정상 precision(1) = 0.8182
뒤바뀜 ravel -> TN,FP,FN,TP = 80 10 20 90
뒤바뀜으로 계산한 recall = 0.8182
두 행렬이 전치 관계인가: True
왜 이렇게 푸는가
confusion_matrix(y_true, y_pred)는 행이 실제, 열이 예측입니다. 따라서 정상 행렬은 이렇게 읽힙니다.
예측 0 예측 1
실제 0 [ 80 , 20 ] TN=80, FP=20
실제 1 [ 10 , 90 ] FN=10, TP=90
- 올바른 재현율 = TP / (TP + FN) = 90 / (90 + 10) = 0.9000
- 올바른 정밀도 = TP / (TP + FP) = 90 / (90 + 20) = 0.8182
인자 순서를 바꾸면 행과 열의 의미가 맞바뀌므로 행렬이 정확히 전치(transpose) 됩니다. 그 결과 대각선 값(TN·TP)은 그대로인데 FP와 FN만 서로 자리를 바꿉니다. 그래서 A가 계산한 "재현율"은 사실 정밀도 0.8182입니다.
이 실수가 위험한 이유는 두 가지입니다. 첫째, 오류 메시지가 전혀 나지 않습니다. 둘째, 두 값이 대체로 비슷한 범위(0.8~0.9)라 숫자만 보고는 이상함을 알아채기 어렵습니다. 정밀도와 재현율이 뒤바뀐 채로 "재현율이 높으니 놓치는 환자가 적다" 같은 해석까지 쓰면 해석 문항까지 함께 틀립니다.
자주 틀리는 지점
- sklearn 평가 함수의 인자 순서를 뒤집는 것.
confusion_matrix,classification_report,accuracy_score,r2_score모두 예외 없이 정답이 먼저, 예측이 나중입니다.(y_test, y_pred)한 덩어리로 외우십시오. cm.ravel()의 순서를tp, fp, fn, tn으로 잘못 받는 것.ravel()은 왼쪽 위부터 가로로 펴므로 언제나tn, fp, fn, tp순입니다. 그리고 이 언패킹은 2×2(이진분류)에서만 성립합니다. 3클래스면 행렬이 3×3이라 값이 9개여서 언패킹 자체가 실패합니다.- 5번을 고르는 것. 순서를 바꿔도 두 배열의 길이와 라벨 종류가 같으므로
ValueError는 나지 않습니다. 조용히 틀리는 것이 이 실수의 핵심입니다.
Q3 해답
정답 코드
import numpy as np
from sklearn.metrics import classification_report, confusion_matrix
loss, acc = model.evaluate(X_test, y_test, verbose=0) # (가)
print('test loss :', round(loss, 4))
print('test accuracy:', round(acc, 4))
pred = model.predict(X_test, verbose=0)
print('pred shape:', pred.shape)
y_pred = (pred > 0.5).astype(int).flatten() # (나)
print('y_pred shape:', y_pred.shape)
print(classification_report(y_test, y_pred)) # (다)
cm = confusion_matrix(y_test, y_pred) # (라)
print(cm)
tn, fp, fn, tp = cm.ravel()
print('TN:', tn, '/ FP:', fp, '/ FN:', fn, '/ TP:', tp)
test loss : 0.068
test accuracy: 0.9766
pred shape: (171, 1)
y_pred shape: (171,)
precision recall f1-score support
0 0.95 0.98 0.97 64
1 0.99 0.97 0.98 107
accuracy 0.98 171
macro avg 0.97 0.98 0.98 171
weighted avg 0.98 0.98 0.98 171
[[ 63 1]
[ 3 104]]
TN: 63 / FP: 1 / FN: 3 / TP: 104
loss·accuracy와 혼동 행렬의 칸 값은 실행할 때마다 조금씩 달라집니다(검증 시 accuracy 0.97~0.98).
shape 두 줄 —pred.shape는(171, 1),y_pred.shape는(171,)— 은 환경과 무관하게 항상 같습니다.
shape 답: pred.shape는 (171, 1)(샘플 171개 × 출력 노드 1개), y_pred.shape는 (171,)입니다.
왜 이렇게 푸는가
- (가)
loss, acc = ...—metrics=['accuracy']로 컴파일했으므로 반환 길이는 2이고 loss가 먼저입니다. - (나)
(pred > 0.5).astype(int).flatten()— 세 조각이 각각 일을 합니다.(pred > 0.5)가True/False배열을 만들고,.astype(int)가 이를 1/0으로 바꾸고,.flatten()이(171, 1)을(171,)로 폅니다. sklearn 지표 함수는 1차원 라벨 배열을 기대하므로 펴 두는 편이 안전합니다. - (다)(라) 인자 순서는
(y_test, y_pred)— 정답이 먼저입니다(Q2).
print(classification_report(...))처럼 print로 감싸는 것을 잊지 마십시오. classification_report는 문자열을 돌려주는 함수라 print 없이 호출하면 노트북 화면에 줄바꿈이 \n 그대로 찍힌 한 줄이 나오거나, 아예 아무것도 보이지 않습니다. 채점자는 화면에 출력된 결과를 봅니다.
자주 틀리는 지점
- (나)를 통째로 빠뜨리고
pred를 그대로classification_report에 넣는 것 — 이 챕터에서 가장 흔한 사고입니다.ValueError: Classification metrics can't handle a mix of binary and continuous targets가 납니다.predict다음 줄에는 반드시 변환 한 줄이 온다고 외워 두십시오. .astype(int)를 빼먹는 것.(pred > 0.5)만으로도True/False가 0/1로 취급되어 결과는 나오지만, 리포트의 클래스 라벨이False/True로 찍혀 지시된 출력 형태와 어긋납니다.- "성능을 출력하시오"에
evaluate한 줄만 찍고 끝내는 것.evaluate(loss·accuracy) +classification_report+confusion_matrix3종 세트를 모두print로 남기는 것이 안전한 답안입니다.
Q4 해답
정답 코드
import numpy as np
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
bin_pred = np.array([[0.83], [0.12], [0.50], [0.91], [0.46]])
multi_pred = np.array([[0.1, 0.7, 0.2],
[0.6, 0.3, 0.1],
[0.2, 0.2, 0.6],
[0.3, 0.5, 0.2],
[0.8, 0.1, 0.1]])
reg_pred = np.array([[151.2], [74.8], [140.5], [205.9], [134.7]])
y_reg_true = np.array([155.0, 70.0, 145.0, 200.0, 130.0])
y_bin = (bin_pred > 0.5).astype(int).flatten() # (가)
y_mul = np.argmax(multi_pred, axis=1) # (나)
y_reg = reg_pred.flatten() # (다)
print('(가) 이진:', y_bin, y_bin.shape)
print('(나) 다중:', y_mul, y_mul.shape)
print('(다) 회귀:', y_reg, y_reg.shape)
print('multi_pred 행 합:', multi_pred.sum(axis=1))
print('mse:', round(mean_squared_error(y_reg_true, y_reg), 3)) # (라-1)
print('mae:', round(mean_absolute_error(y_reg_true, y_reg), 3)) # (라-2)
print('r2 :', round(r2_score(y_reg_true, y_reg), 4)) # (라-3)
# 축을 잘못 지정하면
wrong = np.argmax(multi_pred, axis=0)
print('axis=0 결과:', wrong, wrong.shape)
try:
from sklearn.metrics import classification_report
classification_report(np.array([1, 0, 2, 1, 0]), wrong)
except ValueError as e:
print('axis=0 ->', type(e).__name__, ':', str(e)[:70])
(가) 이진: [1 0 0 1 0] (5,)
(나) 다중: [1 0 2 1 0] (5,)
(다) 회귀: [151.2 74.8 140.5 205.9 134.7] (5,)
multi_pred 행 합: [1. 1. 1. 1. 1.]
mse: 22.926
mae: 4.74
r2 : 0.987
axis=0 결과: [4 0 2] (3,)
axis=0 -> ValueError : Found input variables with inconsistent numbers of samples
축 지정 답: axis=0으로 하면 결과 shape가 (3,) — 샘플 수 5가 아니라 클래스 수 3 이 됩니다. 그 상태로 classification_report(y_test, y_pred)를 호출하면 정답(5개)과 길이가 맞지 않아 ValueError: Found input variables with inconsistent numbers of samples: [5, 3]이 납니다.
왜 이렇게 푸는가
세 유형의 변환은 출력층의 모양에서 그대로 따라 나옵니다.
| 유형 | 출력층 | predict shape |
변환 | 결과 |
|---|---|---|---|---|
| 이진분류 | Dense(1, 'sigmoid') |
(N, 1) 확률 |
(pred > 0.5).astype(int).flatten() |
(N,) 0/1 |
| 다중분류 | Dense(K, 'softmax') |
(N, K) 확률 |
np.argmax(pred, axis=1) |
(N,) 0~K-1 |
| 회귀 | Dense(1) 활성화 없음 |
(N, 1) 예측값 |
pred.flatten() |
(N,) 실수 |
- (가) 세 번째 값
0.50이0이 된 것에 주목하십시오.>는 초과이므로 정확히 0.5인 값은 0으로 갑니다. 별도 지시가 없으면 이 기본 동작을 그대로 쓰면 됩니다. - (나)
axis=1은 "한 행(= 한 샘플) 안에서 가장 큰 값의 위치" 를 찾으라는 뜻입니다. softmax 출력은 행 합이 1인 확률 분포이므로(multi_pred 행 합: [1. 1. 1. 1. 1.]), 그 안에서 가장 큰 자리가 예측 클래스입니다. 클래스 번호는 0부터 시작하므로[0.2, 0.2, 0.6]의 답은2입니다. - (다)
flatten()은 값을 바꾸는 변환이 아니라 모양만 정리하는 작업입니다. 회귀에는 임계값도 argmax도 없습니다.
(라)의 sklearn 회귀 지표 함수들은 인자 순서가 (정답, 예측) 입니다. mse는 잔차를 제곱해 평균하므로 값이 크게 나오고(22.926), mae는 "평균적으로 4.74만큼 빗나간다"로 바로 읽힙니다. r2는 0.987이므로 타깃 변동의 98.7%를 설명한 셈입니다.
자주 틀리는 지점
axis지정을 빠뜨리거나axis=0으로 쓰는 것 — 다중분류에서 가장 잦은 실수입니다.np.argmax(pred)처럼 축을 아예 안 주면 배열 전체에서 최댓값 하나의 평탄화된 인덱스(정수 한 개)가 나옵니다. 다중분류 =axis=1을 한 덩어리로 외우십시오.- 회귀에서
flatten()을 빠뜨리는 것 — sklearn의 mse·mae·r2 함수 자체는(N, 1)을 받아 주기 때문에 여기서는 값이 맞게 나옵니다. 문제는 그다음입니다. 잔차를 직접 계산하면y_test - pred가(N,)대(N, 1)로 브로드캐스팅되어(N, N)행렬이 되고,pd.DataFrame({'actual': y_test, 'pred': pred})로 비교표를 만들면ValueError: Per-column arrays must each be 1-dimensional이 납니다.predict(...).flatten()을 붙여 두는 습관이 이 사고들을 한 번에 막습니다. - 이진분류에
argmax를 쓰는 것. 출력이(N, 1)이라 한 행에 값이 하나뿐이므로argmax는 전부0을 돌려줍니다. 오류는 나지 않고 정확도만 무너집니다.
Q5 해답
잘못된 곳 2군데
print(classification_report(y_test, pred))— 변환된 라벨y_pred대신 확률 배열pred를 넣었습니다.ValueError로 실행이 중단됩니다.cm = confusion_matrix(y_pred, y_test)— 인자 순서가 뒤바뀌었습니다. 오류는 나지 않고 행렬만 전치되어 FP와 FN이 서로 자리를 바꿉니다.
정답 코드
import numpy as np
from sklearn.metrics import classification_report, confusion_matrix
pred = model.predict(X_test, verbose=0)
y_pred = (pred > 0.5).astype(int).flatten()
# --- 오류 ① 재현: 확률을 그대로 투입 ---
try:
classification_report(y_test, pred)
except ValueError as e:
print('①', type(e).__name__, ':', str(e)[:75])
# --- 오류 ② 재현: 효과가 항상 드러나는 고정 예제로 확인 ---
demo_true = np.array([0] * 100 + [1] * 100)
demo_pred = np.array([0] * 80 + [1] * 20 + [0] * 10 + [1] * 90)
print('② 정상 confusion_matrix(y_test, y_pred):\n',
confusion_matrix(demo_true, demo_pred))
print('② 뒤바뀜 confusion_matrix(y_pred, y_test):\n',
confusion_matrix(demo_pred, demo_true))
# 실제 모델 결과에서도 두 행렬은 언제나 전치 관계입니다
cm_ok = confusion_matrix(y_test, y_pred)
cm_bad = confusion_matrix(y_pred, y_test)
print(' 모델 결과도 전치 관계인가:', np.array_equal(cm_ok, cm_bad.T))
print(' FP와 FN이 우연히 같은가:', cm_ok[0, 1] == cm_ok[1, 0])
# --- 수정본 ---
loss, acc = model.evaluate(X_test, y_test, verbose=0)
print('test accuracy:', round(acc, 4))
pred = model.predict(X_test, verbose=0)
y_pred = (pred > 0.5).astype(int).flatten() # 변환 한 줄
print(classification_report(y_test, y_pred)) # 라벨을 넣습니다
cm = confusion_matrix(y_test, y_pred) # 정답이 먼저
print(cm)
① ValueError : Classification metrics can't handle a mix of binary and continuous
② 정상 confusion_matrix(y_test, y_pred):
[[80 20]
[10 90]]
② 뒤바뀜 confusion_matrix(y_pred, y_test):
[[80 10]
[20 90]]
모델 결과도 전치 관계인가: True
FP와 FN이 우연히 같은가: False
test accuracy: 0.9766
precision recall f1-score support
0 0.95 0.98 0.97 64
1 0.99 0.97 0.98 107
accuracy 0.98 171
macro avg 0.97 0.98 0.98 171
weighted avg 0.98 0.98 0.98 171
[[ 63 1]
[ 3 104]]
혼동 행렬의 칸 값과 accuracy는 실행할 때마다 달라집니다(검증 시 accuracy 0.97~0.98).
② 를 모델 결과가 아니라 고정 예제로 보여 주는 이유가 마지막 출력 줄에 있습니다. 학습 결과에 따라 FP와 FN이 우연히 같은 값이 되는 실행이 있고(검증 중 실제로 둘 다 2가 나온 실행이 있었습니다), 그때는 뒤바뀐 행렬이 정상과 겉보기에 똑같아 보여 오류가 완전히 숨어 버립니다. 전치 관계라는 성질 자체는 언제나 성립합니다.
확인할 것은 ① 오류 메시지 문구와 ② 두 행렬이 전치 관계라는 사실입니다.
왜 이렇게 푸는가
두 오류의 성격이 정반대라는 점이 이 문항의 핵심입니다.
① 확률을 그대로 투입 — 시끄럽게 실패합니다. classification_report와 confusion_matrix는 분류 지표이므로 이산적인 라벨만 받습니다. pred는 0.83 같은 연속값이라 "이진(binary) 정답과 연속(continuous) 예측이 섞였다"는 ValueError가 납니다. 바로 위 줄에서 y_pred를 이미 잘 만들어 두고 정작 pred를 넣은 것이므로, 고칠 곳은 인자 이름 한 글자입니다. 시험장에서 이 메시지를 보면 "변환한 변수를 안 썼구나"로 바로 연결하십시오.
② 인자 순서 뒤바뀜 — 조용히 실패합니다. 오류가 없으므로 그대로 제출하게 됩니다. 고정 예제에서 뒤바뀐 [[80, 10], [20, 90]]은 정상 [[80, 20], [10, 90]]의 전치입니다. 대각선(TN=80, TP=90)은 그대로라 정확도는 변하지 않는데, FP와 FN만 서로 자리를 바꾸므로 정밀도와 재현율이 맞바뀝니다. 혼동 행렬을 해석하는 서술 문항이 이어지면 그 문항까지 함께 틀립니다.
두 오류 모두 예방책은 하나입니다. 평가 블록을 evaluate → predict → 변환 → (y_test, y_pred) 순서로 지표 함수 라는 고정된 순서로 통째로 외워 두는 것입니다. 이 순서에서 벗어나는 줄이 보이면 그 줄이 범인입니다.
자주 틀리는 지점
- 오류를 하나만 찾고 넘어가는 것. ①에서 실행이 멈추므로 ②는 실행조차 되지 않아 발견하기 어렵습니다. ①을 고친 뒤 반드시 아래 줄들을 다시 훑으십시오.
- ①을 고친다면서
classification_report(y_test, pred.flatten())으로 바꾸는 것.flatten()은 모양만 펼 뿐 여전히 확률이라 같은ValueError가 납니다. 필요한 것은 임계값 판정입니다. - ②를 발견하고도 "값이 비슷하니 괜찮다"고 넘기는 것. 정확도는 같지만 정밀도·재현율은 완전히 뒤바뀐 값입니다.
- "행렬을 눈으로 보고 이상하면 알아채겠지"라고 믿는 것. FP와 FN이 우연히 같은 값이 되면 뒤바뀐 행렬이 정상과 완전히 동일하게 출력됩니다. 눈으로 검증할 수 없는 실수이므로, 애초에
(y_test, y_pred)순서를 손이 기억하게 만드는 수밖에 없습니다.