컴파일과 학습
이 챕터에서 배우는 것 — Ch04에서 만든 빈 모델에 "무엇을 줄일지(loss)", "어떻게 줄일지(optimizer)", "무엇을 보여줄지(metrics)"를 정해 준 뒤 실제로 학습을 돌립니다. 특히 손실 함수 선택 규칙은 이 교재 전체에서 가장 자주 출제되는 단일 지점이며, 이 챕터의 분기도가 그 정본입니다.
Ch04까지 우리는 층을 쌓았습니다. 그런데 층을 쌓기만 한 모델은 아직 아무것도 배우지 못한 상태입니다. model.summary()가 보여 주는 파라미터 수만큼의 가중치가 무작위 값으로 채워져 있을 뿐입니다.
이 가중치들을 데이터에 맞게 고쳐 나가려면 두 가지를 먼저 정해야 합니다. 틀린 정도를 재는 자와 재어 본 뒤 가중치를 고치는 방법입니다. 이 두 가지를 모델에 붙이는 단계가 compile이고, 붙인 뒤 실제로 데이터를 흘려보내는 단계가 fit입니다.
1. compile이 정하는 세 가지
한 줄 요약: compile은 loss(무엇을 줄일지), optimizer(어떻게 줄일지), metrics(사람이 볼 성적표)를 모델에 등록하는 한 줄짜리 설정 단계입니다.
model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy'])
세 인자의 성격은 서로 다릅니다.
| 인자 | 역할 | 학습에 영향 | 시험에서 바뀌는가 |
|---|---|---|---|
loss |
예측이 정답에서 얼마나 벗어났는지 재는 손실 함수 | 있음 — 모델이 실제로 줄이는 값 | 문제 유형마다 바뀜 (핵심) |
optimizer |
손실을 줄이는 방향으로 가중치를 갱신하는 방법 | 있음 | 거의 'adam' 고정 |
metrics |
학습 중 화면과 history에 함께 찍히는 참고 지표 |
없음 | 분류는 ['accuracy'], 회귀는 ['mae'] |
여기서 가장 자주 오해가 생기는 지점이 metrics입니다. metrics에 무엇을 넣든 모델이 학습하는 방향은 달라지지 않습니다. 모델은 오직 loss만 줄입니다. metrics는 그 과정을 사람이 읽을 수 있는 숫자로 옆에 띄워 주는 계기판일 뿐입니다.
⚠️ 시험 포인트 —
loss와metrics를 바꿔 넣지 마십시오
loss='accuracy'는 동작하지 않습니다. 정확도는 "맞았다/틀렸다"를 세는 계단식 값이라 미세하게 미분할 수 없고, 그래서 가중치를 고치는 기준이 될 수 없습니다.compile()은 통과하더라도fit()을 부르는 순간ValueError: Could not interpret loss identifier: accuracy가 납니다. 반대로metrics=['binary_crossentropy']처럼 손실 함수를 지표 자리에 넣으면,compile()은 통과하지만fit()에서 타깃 shape가 맞지 않아 오류가 나거나 통과하더라도 의미가 없습니다.loss와metrics는 서로의 자리에 넣는 값이 아닙니다. 줄일 대상은loss, 볼 대상은metrics로 자리를 고정해 외우십시오.
2. 손실 함수 선택 — 이 교재의 정본
한 줄 요약: 손실 함수는 취향이 아니라 타깃의 형태가 결정합니다. 위 분기도의 질문 두 개("숫자인가 범주인가", "클래스가 2개인가", 다중이면 "정수인가 원-핫인가")에 답하면 loss와 출력층이 한 짝으로 정해집니다.
이 분기도가 교재 전체에서 손실 함수 판단의 유일한 기준입니다. Ch04에서 본 출력층 선택 분기도는 출력층만 다뤘고, 그 짝이 되는 loss는 여기서 확정합니다.
표로 다시 정리하면 이렇습니다. 시험장에서 이 표 한 장만 떠올리면 딥러닝 문항의 절반이 해결됩니다.
| 문제 유형 | 타깃(y)의 형태 | 출력층 | loss |
metrics |
|---|---|---|---|---|
| 이진분류 | 0, 1 정수 |
Dense(1, activation='sigmoid') |
'binary_crossentropy' |
['accuracy'] |
| 다중분류 (K클래스) | 0, 1, 2 … 정수 라벨 |
Dense(K, activation='softmax') |
'sparse_categorical_crossentropy' |
['accuracy'] |
| 다중분류 (K클래스) | [0,1,0] … 원-핫 |
Dense(K, activation='softmax') |
'categorical_crossentropy' |
['accuracy'] |
| 회귀 | 연속된 숫자 | Dense(1) — 활성화 없음 |
'mse' (또는 'mae') |
['mae'] |
출력층과 loss는 반드시 짝으로 움직입니다. 한쪽만 바꾸면 학습이 조용히 망가지거나 오류가 납니다. 유형이 바뀌었을 때 손대야 하는 곳은 항상 이 두 줄(그리고 metrics 한 줄)입니다.
# 이진분류
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
# 다중분류 — y가 0,1,2 정수일 때
model.add(Dense(3, activation='softmax'))
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
# 회귀
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse', metrics=['mae'])
sparse라는 접두어의 의미를 한 번만 짚어 두면 헷갈리지 않습니다. sparse_가 붙은 쪽은 "라벨이 정수 하나로 압축(sparse)되어 있다" 는 뜻입니다. 즉 y = [0, 2, 1, 2] 형태입니다. 접두어가 없는 categorical_crossentropy는 라벨이 이미 [[1,0,0], [0,0,1], [0,1,0], [0,0,1]] 처럼 펼쳐진 원-핫 형태일 때 씁니다.
⚠️ 시험 포인트 — 정수 라벨에
categorical_crossentropy를 쓰면 shape 오류가 납니다
가장 흔한 실수입니다.y_train이0,1,2정수인데loss='categorical_crossentropy'로 컴파일하면, 모델은(N, 3)형태의 정답을 기대하는데(N,)이 들어와 shape 불일치 오류가 발생합니다. 판단 순서를 "y를 먼저 찍어 본다"로 고정하십시오.print(y_train[:5])를 실행해[0 2 1 2 0]처럼 나오면sparse_를 붙이고,[[1 0 0] ...]처럼 나오면 붙이지 않습니다. 정수 라벨을 원-핫으로 바꾸고 싶다면tf.keras.utils.to_categorical(y_train)을 쓴 뒤categorical_crossentropy로 맞추면 됩니다. 어느 쪽이든 loss와 y의 형태가 일치하기만 하면 됩니다.
회귀에서 mse(mean squared error, 평균제곱오차)와 mae(mean absolute error, 평균절대오차) 중 무엇을 쓸지 묻는다면, 문제에 지시가 없는 한 loss='mse'를 기본으로 하고 metrics=['mae']로 사람이 읽기 쉬운 지표를 함께 띄우는 조합이 무난합니다. mse는 오차를 제곱하므로 큰 오차에 더 크게 반응하고, mae는 이상치의 영향을 덜 받습니다.
⚠️ 시험 포인트 — 회귀 출력층에 활성화 함수를 넣지 마십시오
회귀 출력층은Dense(1)입니다. 여기에activation='sigmoid'를 붙이면 출력이 0~1로 눌려 집값이나 잔여 수명처럼 범위가 넓은 값을 절대 맞힐 수 없게 됩니다. 손실은 줄어들지 않고mae가 큰 값에서 멈춥니다. 분류 코드를 복사해 회귀로 고칠 때 loss만mse로 바꾸고 출력층의sigmoid를 지우지 않는 실수가 특히 잦습니다.
3. optimizer와 학습률
한 줄 요약: optimizer는 손실이라는 골짜기를 내려가는 방법이고, 학습률(learning rate)은 한 걸음의 보폭입니다. 보폭이 너무 작으면 느리고, 너무 크면 골짜기 바닥을 지나쳐 튕겨 나갑니다.
시험에서는 사실상 optimizer='adam' 하나만 알면 됩니다. Adam은 학습이 진행되는 동안 보폭을 스스로 조절해 주기 때문에, 대부분의 정형 데이터 문제에서 기본값으로 무난하게 수렴합니다. 지시가 없다면 문자열 'adam'을 그대로 쓰십시오.
학습률을 직접 조절해야 할 때만 객체 형태로 바꿔 씁니다. Adam의 기본 학습률은 0.001입니다.
from tensorflow.keras.optimizers import Adam
model.compile(optimizer=Adam(learning_rate=0.001),
loss='binary_crossentropy',
metrics=['accuracy'])
손실이 전혀 줄지 않거나 nan으로 튀면 학습률이 너무 큰 경우를 의심하고 0.0001 수준으로 낮춰 봅니다. 반대로 손실이 아주 느리게만 줄어든다면 학습률을 키우거나 epochs를 늘립니다. 다만 시험 시간 안에서는 학습률을 튜닝하기보다 'adam' 기본값으로 두고 다음 문항으로 넘어가는 편이 점수에 유리합니다.
4. fit — epochs, batch_size, step
한 줄 요약: batch_size는 한 번에 몇 행씩 보고 가중치를 고칠지, epochs는 전체 데이터를 몇 번 반복해서 볼지입니다.
위 도해는 검증 데이터를 떼지 않고 1,000행 전부를 학습에 쓰는 경우를 전제로 그린 것입니다. 이 전제에서 batch_size=32라면, 모델은 32행을 보고 가중치를 한 번 고치는 동작(= 1 step)을 반복합니다. 1,000행을 다 훑으려면 ceil(1000 / 32) = 32 step이 필요하고, 이 한 바퀴가 1 epoch입니다. epochs=50이면 한 바퀴를 50번 반복하므로 가중치는 총 1,600번 갱신됩니다.
여기서 한 가지 규칙을 함께 외워 두어야 합니다. fit에 validation_split이 있으면 그 몫이 먼저 빠지고, 남은 행만 실제 학습에 쓰입니다. 그래서 step 수는 전체 행이 아니라 검증분을 뺀 행 수로 셉니다.
step 수 = ceil( 학습에 실제로 쓰이는 행 수 / batch_size )
validation_split 없음 : ceil(1000 / 32) = 32 step
validation_split=0.2 : 1000 x 0.8 = 800행 → ceil(800 / 32) = 25 step
같은 1,000행·같은 batch_size=32인데 검증 20%를 떼는 순간 step이 32에서 25로 줄어듭니다. 학습 중 화면에 찍히는 진행 막대(25/25)가 예상과 다르면 대개 이 계산을 빠뜨린 것입니다.
| 인자 | 의미 | 시험 표준값 | 크게 하면 | 작게 하면 |
|---|---|---|---|---|
epochs |
전체 데이터 반복 횟수 | 50~100 | 오래 걸리고 과적합 위험 | 덜 학습되어 성능 부족 |
batch_size |
한 step에 보는 행 수 | 32 (또는 16, 64) | 빠르지만 갱신 횟수가 줄어 거칠게 수렴 | 느리지만 촘촘하게 수렴 |
verbose |
진행 출력 방식 | 1 (또는 0으로 조용히) |
— | — |
history = model.fit(X_train_scaled, y_train,
epochs=50, batch_size=32,
validation_split=0.2, verbose=1)
출력 요지: X_train_scaled가 1,000행이라면 검증 20%가 먼저 빠져 800행으로 학습하므로, 진행 막대는 32/32가 아니라 25/25 로 찍히는 줄이 50번 반복됩니다.
batch_size를 지정하지 않으면 기본값 32가 쓰입니다. 문제에서 값을 지시했다면 그 숫자를 그대로 옮겨 적으십시오.
⚠️ 시험 포인트 —
epochs를 무작정 늘리는 것은 대책이 아닙니다
"성능이 안 나오니epochs=500으로 올린다"는 흔한 유혹인데, 어느 지점을 지나면 학습 데이터에만 맞춰지는 과적합이 시작되어 평가(테스트) 데이터 성능은 오히려 떨어집니다. 올바른 대응은epochs를 넉넉히 주되EarlyStopping콜백으로 알아서 멈추게 하는 것이며, 이는 Ch06에서 다룹니다. 이 챕터에서는 "몇 epoch가 적절한지는 학습 곡선을 보고 판단한다"까지만 기억하면 됩니다.
5. 검증 데이터를 떼어내는 두 가지 방법
한 줄 요약: validation_split=0.2는 학습 데이터의 20%를 검증 데이터로 떼어 매 epoch마다 성적을 재는 인자입니다. 평가(테스트) 데이터는 여기에 관여하지 않습니다.
데이터가 세 갈래로 쓰인다는 점을 먼저 정리하겠습니다.
| 이름 | 만드는 시점 | 쓰임 |
|---|---|---|
| 학습 데이터 | train_test_split |
가중치를 실제로 고치는 데 사용 |
| 검증 데이터 | fit의 validation_split |
매 epoch 끝에 성적을 재어 과적합 시점을 감시 |
| 평가(테스트) 데이터 | train_test_split |
마지막에 딱 한 번, 최종 성능 측정 |
검증 데이터를 지정하지 않으면 history에 val_loss가 생기지 않고, 학습 곡선을 두 줄로 그릴 수 없으며, Ch06의 EarlyStopping도 감시할 대상을 잃습니다. 딥러닝 학습 문항에서 validation_split(또는 validation_data)은 사실상 필수 인자로 보십시오.
떼어낸 검증 데이터는 가중치를 고치는 데 쓰이지 않는다는 점을 기억하십시오. §4에서 본 step 수를 검증분을 뺀 행 수로 세는 이유가 여기 있습니다.
떼어내는 방법은 두 가지입니다.
# 방법 A — 비율만 지정 (시험 기본형)
history = model.fit(X_train_scaled, y_train,
epochs=50, batch_size=32,
validation_split=0.2)
# 방법 B — 검증 데이터를 직접 만들어 전달
from sklearn.model_selection import train_test_split
X_tr, X_val, y_tr, y_val = train_test_split(
X_train_scaled, y_train, test_size=0.2, random_state=42, stratify=y_train)
history = model.fit(X_tr, y_tr,
epochs=50, batch_size=32,
validation_data=(X_val, y_val))
두 방법은 함께 쓰지 않습니다. 문제에 "검증 데이터 20%"라는 지시만 있으면 방법 A가 간결하고, "검증 데이터를 계층 추출(stratify)로 나누시오"처럼 분리 방식을 지시하면 방법 B를 씁니다.
⚠️ 시험 포인트 —
validation_split은 데이터의 "뒤쪽"을 그대로 잘라냅니다
validation_split은 섞지 않고 전달된 배열의 마지막 20%를 검증 데이터로 떼어냅니다. 만약 타깃이 정렬된 데이터(예: 앞쪽에 클래스 0, 뒤쪽에 클래스 1이 몰려 있는 데이터)를 그대로 넣으면, 검증 데이터가 한 클래스로만 채워져val_accuracy가 이상한 값으로 고정됩니다. 앞 단계에서train_test_split을shuffle=True(기본값)로 통과했다면 이미 섞여 있으므로 안전합니다. 정렬 상태가 의심되면print(y_train[:20])으로 확인하거나 방법 B를 쓰십시오.
6. history — 학습이 어떻게 진행됐는지 읽기
한 줄 요약: fit은 학습 기록을 담은 history 객체를 돌려주며, history.history는 epoch별 값이 리스트로 들어 있는 딕셔너리입니다.
history = model.fit(...)
print(history.history.keys())
출력 요지: dict_keys(['accuracy', 'loss', 'val_accuracy', 'val_loss']) 네 개의 키가 나옵니다. 키가 찍히는 순서는 보장되지 않으므로(회귀에서는 ['loss', 'mae', ...] 순으로 나오기도 합니다) 순서로 기억하지 말고 이름으로 찾으십시오. 항상 같은 순서로 보고 싶다면 print(sorted(history.history.keys()))를 쓰면 됩니다. 키 이름의 규칙은 명확합니다.
loss/accuracy— 학습 데이터에서 잰 값val_접두어가 붙은 것 — 검증 데이터에서 잰 값 (검증을 지정했을 때만 생김)accuracy자리는compile의metrics에 넣은 이름 그대로입니다. 회귀에서metrics=['mae']로 했다면 키는mae,val_mae가 됩니다.
각 값은 epoch 수만큼의 길이를 가진 리스트이므로 그대로 그래프로 그릴 수 있습니다. 아래는 모든 문제 유형에서 재사용할 수 있는 학습 곡선 플롯 함수입니다.
import matplotlib.pyplot as plt
def plot_history(history):
"""학습 곡선 2패널(손실 / 지표)을 그립니다."""
hist = history.history
# metrics로 넣은 지표 이름을 자동으로 찾습니다 (accuracy, mae 등)
metric = [k for k in hist.keys() if not k.startswith('val_') and k != 'loss']
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
axes[0].plot(hist['loss'], label='train loss')
if 'val_loss' in hist:
axes[0].plot(hist['val_loss'], '--', label='val loss')
axes[0].set_xlabel('epoch')
axes[0].set_ylabel('loss')
axes[0].set_title('Loss')
axes[0].legend()
if metric:
m = metric[0]
axes[1].plot(hist[m], label='train ' + m)
if 'val_' + m in hist:
axes[1].plot(hist['val_' + m], '--', label='val ' + m)
axes[1].set_xlabel('epoch')
axes[1].set_ylabel(m)
axes[1].set_title(m)
axes[1].legend()
plt.tight_layout()
plt.show()
plot_history(history)
출력 요지: 왼쪽에 손실 곡선 두 줄, 오른쪽에 정확도(또는 mae) 곡선 두 줄이 그려진 그림 한 장이 뜹니다.
곡선을 읽는 기준은 단순합니다. 두 줄이 함께 내려가면 정상, train은 내려가는데 val이 어느 지점부터 올라가면 그 지점이 과적합 시작점, 두 줄 모두 높은 곳에서 평평하면 학습 부족입니다. 이 판별 기준과 대응 방법은 Ch06에서 곡선 유형별로 자세히 다룹니다. 여기서는 "학습이 끝나면 반드시 곡선을 그려 확인한다"는 습관까지만 만들어 두십시오.
history에서 특정 값을 직접 꺼내 쓸 수도 있습니다.
print('마지막 epoch val_loss:', history.history['val_loss'][-1])
print('가장 낮은 val_loss:', min(history.history['val_loss']))
print('총 학습 epoch 수:', len(history.history['loss']))
마지막 줄은 Ch06에서 EarlyStopping이 실제로 몇 epoch에서 멈췄는지 확인할 때 유용합니다.
시험장 표준 코드
아래는 컴파일부터 학습, 곡선 확인까지 한 벌로 도는 완결 코드입니다. 데이터는 sklearn 내장 유방암 진단 데이터(이진분류)를 씁니다. 시험장에서는 데이터 로딩 부분만 문제의 데이터로 바꾸고, 유형이 다르면 표시해 둔 교체 지점 세 줄만 고치면 됩니다.
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense, Dropout
# 재현성 확보
np.random.seed(42)
tf.random.set_seed(42)
# 1) 데이터 준비 --------------------------------------------------
data = load_breast_cancer()
X, y = data.data, data.target
print('X shape:', X.shape, '/ y 앞 5개:', y[:5]) # loss 판단의 근거
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) # 학습 데이터로만 fit
X_test_scaled = scaler.transform(X_test) # 평가 데이터는 transform만
# 2) 모델 구성 ----------------------------------------------------
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')) # 교체 지점 (1) 출력층
# 3) 컴파일 -------------------------------------------------------
model.compile(optimizer='adam',
loss='binary_crossentropy', # 교체 지점 (2) 손실 함수
metrics=['accuracy']) # 교체 지점 (3) 지표
# 4) 학습 ---------------------------------------------------------
history = model.fit(X_train_scaled, y_train,
epochs=50, batch_size=32,
validation_split=0.2,
verbose=1)
print(history.history.keys())
print('마지막 val_loss:', round(history.history['val_loss'][-1], 4))
print('마지막 val_accuracy:', round(history.history['val_accuracy'][-1], 4))
출력 요지: X shape: (569, 30) / y 앞 5개: [0 0 0 0 0]이 먼저 찍히고, epoch마다 loss, accuracy, val_loss, val_accuracy 네 값이 한 줄씩 50줄 출력됩니다. 마지막에 dict_keys([...])와 검증 성적 두 줄이 나옵니다. 검증 정확도는 0.98~1.00 구간에서 형성됩니다(학습 데이터 398행의 20%인 검증 세트가 80건뿐이라 정확도 눈금이 1/80 = 0.0125 단위로 성깁니다). 라이브러리 버전과 실행 시점에 따라 소수점 아래는 달라지므로, 자기 결과가 이 구간 안에 들어오는지만 확인하면 됩니다.
유형을 바꿀 때 손대는 곳은 위 세 줄뿐입니다.
# 다중분류 (K=3, y가 0/1/2 정수 라벨일 때)
model.add(Dense(3, activation='softmax')) # (1)
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy', # (2)
metrics=['accuracy']) # (3)
# 회귀
model.add(Dense(1)) # (1) 활성화 없음
model.compile(optimizer='adam',
loss='mse', # (2)
metrics=['mae']) # (3)
핵심 요약
| 항목 | 기억할 것 |
|---|---|
compile 3인자 |
loss(줄일 대상) / optimizer(줄이는 방법, 'adam' 고정) / metrics(볼 지표, 학습에 영향 없음) |
| 이진분류 | Dense(1,'sigmoid') + binary_crossentropy + ['accuracy'] |
| 다중분류 | Dense(K,'softmax') + 정수 라벨이면 sparse_categorical_crossentropy, 원-핫이면 categorical_crossentropy |
| 회귀 | Dense(1) 활성화 없음 + mse + ['mae'] |
| loss 판단 순서 | print(y_train[:5])로 타깃 형태를 먼저 확인 → 분기도의 질문에 답 → loss와 출력층을 짝으로 결정 |
fit 표준 |
epochs=50~100, batch_size=32, validation_split=0.2. epochs만 늘리는 것은 과적합 대책이 아님(Ch06) |
| step 수 계산 | ceil(학습에 실제로 쓰이는 행 수 / batch_size). validation_split이 먼저 차감되므로 1,000행·0.2·batch_size=32면 ceil(800/32) = 25 step |
history |
history.history는 딕셔너리. loss/val_loss/accuracy/val_accuracy 키로 학습 곡선을 두 줄씩 그려 확인 |
Ch05 확인 문제 — 컴파일과 학습
본 문제는 공개된 출제 기준에 따른 유형 연습 문제입니다. 기출 원문이 아닙니다.
이 챕터의 범위는
compile()과fit(), 그리고history까지입니다.
콜백(EarlyStopping등)은 Ch06,evaluate·predict는 Ch07에서 다루므로 여기서는 쓰지 않습니다.Q1은 이 교재 전체에서 가장 자주 출제되는 지점(손실 함수와 출력층의 짝)입니다. 틀렸다면 Ch05 2절의 표로 반드시 되돌아가십시오.
총 6문항 (객관식 2 / 코드 빈칸 2 / 오류 찾기 2), 권장 소요 시간 25분.
공통 import는 다음과 같다고 가정합니다.
python import numpy as np import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Input, Dense
문제
Q1. (객관식 · 기본) 손실 함수와 출력층의 짝 — 네 유형 판별
네 개의 서로 다른 과제에서 전처리를 마친 뒤 print(y_train[:5])와 print(y_train.shape)를 실행한 결과입니다.
| 상황 | 과제 | y_train[:5] 출력 |
y_train.shape |
|---|---|---|---|
| (가) | 고객 이탈 여부 예측 | [0 1 0 0 1] |
(2100,) |
| (나) | 설비 불량 유형 3종 분류 | [1 1 1 0 2] |
(1600,) |
| (다) | 설비 불량 유형 3종 분류 | [[0. 1. 0.] [1. 0. 0.] [0. 0. 1.] ...] |
(1600, 3) |
| (라) | 장비 잔여 수명(일) 예측 | [199.4 235.6 123.7 121. 158.2] |
(640,) |
각 상황의 출력층과 loss 를 옳게 짝지은 것은?
- (가)
Dense(1,'sigmoid')+binary_crossentropy/ (나)Dense(3,'softmax')+categorical_crossentropy/ (다)Dense(3,'softmax')+sparse_categorical_crossentropy/ (라)Dense(1)+mse - (가)
Dense(2,'softmax')+categorical_crossentropy/ (나)Dense(3,'softmax')+sparse_categorical_crossentropy/ (다)Dense(3,'softmax')+categorical_crossentropy/ (라)Dense(1,'sigmoid')+mse - (가)
Dense(1,'sigmoid')+binary_crossentropy/ (나)Dense(3,'softmax')+sparse_categorical_crossentropy/ (다)Dense(3,'softmax')+categorical_crossentropy/ (라)Dense(1)+mse - (가)
Dense(1,'sigmoid')+binary_crossentropy/ (나)Dense(1,'softmax')+sparse_categorical_crossentropy/ (다)Dense(3,'sigmoid')+categorical_crossentropy/ (라)Dense(1)+binary_crossentropy - (가)
Dense(1,'relu')+mse/ (나)Dense(3,'softmax')+categorical_crossentropy/ (다)Dense(3,'softmax')+sparse_categorical_crossentropy/ (라)Dense(1,'sigmoid')+mae
Q2. (객관식 · 기본) compile의 세 인자
model.compile(optimizer=..., loss=..., metrics=...)에 대한 설명 중 옳지 않은 것은?
loss는 모델이 학습 과정에서 실제로 줄여 나가는 값이다.metrics에 넣은 지표는 학습 중 화면과history에 함께 기록되지만, 가중치가 갱신되는 방향에는 영향을 주지 않는다.- 같은 데이터·같은 seed·같은 모델에서
metrics=['accuracy']를metrics=['mae']로만 바꾸면 학습되는 가중치와loss값이 달라진다. optimizer는 문제에 지시가 없으면 문자열'adam'을 쓰는 것이 무난하다.loss자리에'accuracy'를 넣으면 학습을 시작할 수 없다.
Q3. (코드 빈칸 · 기본) 이진분류 컴파일과 학습
전처리를 마친 X_train의 shape는 (1400, 10)이고, y_train은 0 또는 1인 이진분류 타깃입니다.
학습 데이터의 20%를 검증 데이터로 떼어 epochs=50, batch_size=32로 학습시키십시오.
(가)에는 손실 함수를, (나)에는 지표를, (다)에는 검증 데이터를 떼어내는 인자를 채우십시오.
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense
np.random.seed(42); tf.random.set_seed(42)
X_train = np.random.rand(1400, 10)
y_train = (X_train[:, 0] * 2 + X_train[:, 1] > 1.5).astype(int)
print('y_train[:5] =', y_train[:5], '/ 클래스:', np.unique(y_train))
model = Sequential()
model.add(Input(shape=(X_train.shape[1],)))
model.add(Dense(64, activation='relu'))
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='adam',
loss=______(가)______,
metrics=______(나)______)
history = model.fit(X_train, y_train,
epochs=50, batch_size=32,
______(다)______,
verbose=1)
print('history 키:', sorted(history.history.keys()))
추가로, 이 코드가 출력하는 history 키 4개를 쓰십시오.
Q4. (코드 빈칸 · 응용) 다중분류 컴파일과 학습, 그리고 step 수
X_train의 shape는 (2000, 12), y_train은 아래 출력에서 보듯 정수 라벨입니다.
epochs=60, batch_size=50으로 학습하고, 학습 데이터의 20%를 검증 데이터로 씁니다.
(가)에는 클래스 개수를 데이터에서 뽑아내는 코드를, (나)에는 출력층 한 줄을, (다)에는 손실 함수를, (라)에는 검증 인자를 채우십시오.
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense
np.random.seed(42); tf.random.set_seed(42)
X_train = np.random.rand(2000, 12)
y_train = np.random.randint(0, 3, 2000)
print('y_train[:8] =', y_train[:8], '/ shape =', y_train.shape)
n_class = ______(가)______
model = Sequential()
model.add(Input(shape=(X_train.shape[1],)))
model.add(Dense(64, activation='relu'))
model.add(Dense(32, activation='relu'))
model.add(______(나)______)
model.compile(optimizer='adam',
loss=______(다)______,
metrics=['accuracy'])
history = model.fit(X_train, y_train,
epochs=60, batch_size=50,
______(라)______,
verbose=1)
추가로 다음 두 가지에 답하십시오.
- 이 설정에서 1 epoch당 step 수는 몇입니까? 계산식과 함께 쓰십시오.
y_train을tf.keras.utils.to_categorical(y_train)으로 바꾼다면, 위 코드에서 어느 줄을 어떻게 고쳐야 합니까?
Q5. (오류 찾기 · 기본) 다중분류 학습에서 나는 오류
아래 코드는 model.compile()까지는 아무 문제 없이 지나가지만 model.fit()에서 오류가 나며 멈춥니다.
잘못된 줄 한 곳을 지적하고, 서로 다른 두 가지 방법으로 고친 코드를 각각 쓰십시오.
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense
np.random.seed(42); tf.random.set_seed(42)
X_train = np.random.rand(600, 8)
y_train = np.digitize(X_train[:, 0] * 3 + X_train[:, 1], [0.9, 1.7, 2.5])
print('y_train[:6] =', y_train[:6], '/ shape =', y_train.shape)
model = Sequential()
model.add(Input(shape=(X_train.shape[1],)))
model.add(Dense(32, activation='relu'))
model.add(Dense(4, activation='softmax'))
model.compile(optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy'])
history = model.fit(X_train, y_train,
epochs=20, batch_size=32,
validation_split=0.2, verbose=0)
Q6. (오류 찾기 · 응용) 회귀 문제로 바꾸다 남은 흔적
y_train은 장비의 잔여 수명(일)으로, 값의 범위는 약 88 ~ 278입니다.
이진분류 코드를 복사해 회귀 문제로 고치던 중 두 군데를 잘못 두었습니다. 각각을 지적하고 수정한 코드를 쓰십시오.
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense
np.random.seed(42); tf.random.set_seed(42)
X_train = np.random.rand(800, 7)
y_train = (120 * X_train[:, 0] + 60 * X_train[:, 1] + 30 * X_train[:, 2]
+ 80 + np.random.randn(800) * 5)
print('y_train[:4] =', np.round(y_train[:4], 1))
model = Sequential()
model.add(Input(shape=(X_train.shape[1],)))
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='adam',
loss='accuracy',
metrics=['mse'])
history = model.fit(X_train, y_train,
epochs=100, batch_size=32,
validation_split=0.2, verbose=0)
해답 및 해설 보기
Q1 해답
정답: 3번
| 상황 | 판단 근거 | 출력층 | loss |
|---|---|---|---|
| (가) | 값이 0/1 두 종류 → 이진분류 |
Dense(1, activation='sigmoid') |
'binary_crossentropy' |
| (나) | 값이 0/1/2, shape가 (1600,) → 다중분류 정수 라벨 |
Dense(3, activation='softmax') |
'sparse_categorical_crossentropy' |
| (다) | shape가 (1600, 3), 각 행이 [0. 1. 0.] → 다중분류 원-핫 |
Dense(3, activation='softmax') |
'categorical_crossentropy' |
| (라) | 소수점 연속값 → 회귀 | Dense(1) — 활성화 없음 |
'mse' (또는 'mae') |
정답 코드 (네 유형이 실제로 학습되는지 확인)
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense
from tensorflow.keras.utils import to_categorical
np.random.seed(42); tf.random.set_seed(42)
X = np.random.rand(200, 6)
cases = [
("(가) 이진분류", np.random.randint(0, 2, 200),
Dense(1, activation='sigmoid'), 'binary_crossentropy', ['accuracy']),
("(나) 다중분류 정수라벨", np.random.randint(0, 3, 200),
Dense(3, activation='softmax'), 'sparse_categorical_crossentropy', ['accuracy']),
("(다) 다중분류 원-핫", to_categorical(np.random.randint(0, 3, 200), 3),
Dense(3, activation='softmax'), 'categorical_crossentropy', ['accuracy']),
("(라) 회귀", np.random.rand(200) * 200 + 50,
Dense(1), 'mse', ['mae']),
]
for name, yy, out, loss, met in cases:
m = Sequential([Input(shape=(6,)), Dense(16, activation='relu'), out])
m.compile(optimizer='adam', loss=loss, metrics=met)
m.fit(X, yy, epochs=2, batch_size=32, verbose=0)
print(f"{name:22s} loss={loss:<32s} 출력 shape={m.output_shape} -> 학습 성공")
(가) 이진분류 loss=binary_crossentropy 출력 shape=(None, 1) -> 학습 성공
(나) 다중분류 정수라벨 loss=sparse_categorical_crossentropy 출력 shape=(None, 3) -> 학습 성공
(다) 다중분류 원-핫 loss=categorical_crossentropy 출력 shape=(None, 3) -> 학습 성공
(라) 회귀 loss=mse 출력 shape=(None, 1) -> 학습 성공
왜 이렇게 푸는가
판단 순서는 언제나 y를 먼저 찍어 보는 것입니다. 문제 설명의 표현("분류하시오", "예측하시오")이 아니라 타깃 배열의 실제 형태가 loss를 결정합니다. Ch05 분기도의 질문 순서를 그대로 따르면 됩니다.
- 숫자인가 범주인가? — (라)처럼 소수점이 붙은 연속값이면 회귀입니다.
Dense(1)에 활성화 함수를 붙이지 않고mse를 씁니다. - 클래스가 2개인가? — (가)처럼
0과1만 있으면 이진분류입니다. 출력은 "1일 확률" 하나이므로Dense(1,'sigmoid')+binary_crossentropy입니다. - 정수인가 원-핫인가? — (나)와 (다)는 같은 과제이지만 라벨 형태가 다릅니다.
shape를 보면 즉시 갈립니다.(1600,)처럼 1차원이면 정수 라벨이므로sparse_를 붙이고,(1600, 3)처럼 2차원이면 이미 펼쳐진 원-핫이므로sparse_를 붙이지 않습니다. 출력층은 두 경우 모두Dense(3,'softmax')로 같습니다.
sparse_라는 접두어를 "라벨이 정수 하나로 압축되어 있다" 로 읽으면 헷갈리지 않습니다.
자주 틀리는 지점
- 1번·5번처럼 (나)와 (다)를 맞바꾸는 것 — 이 챕터에서 가장 흔한 오답입니다. 두 상황의 과제 설명이 완전히 같기 때문에,
shape를 확인하지 않으면 반드시 헷갈립니다.print(y_train.shape)한 줄이 유일한 예방책입니다. - 2번처럼 이진분류에
Dense(2,'softmax')를 쓰는 것 — 원-핫으로 바꾸면 동작은 하지만, 시험 표준 답안은Dense(1,'sigmoid')입니다. 굳이 노드를 2개로 늘려 답을 복잡하게 만들 이유가 없습니다. - 2번·5번처럼 회귀 출력층에
sigmoid를 붙이는 것 — 출력이 0~1로 눌려 88~278 범위의 값을 절대 맞힐 수 없습니다. Q6에서 그 결과를 숫자로 확인합니다. - 4번처럼 다중분류 출력층 units를 클래스 수와 다르게 두는 것(
Dense(1,'softmax')) — softmax는 여러 노드의 값을 합이 1이 되도록 나누는 함수인데 노드가 1개면 항상 1.0만 나옵니다.
Q2 해답
정답: 3번 (옳지 않은 설명)
metrics를 바꿔도 학습되는 가중치와 loss 값은 전혀 달라지지 않습니다.
정답 코드 (선택지 3 반증)
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense
def train_with(metrics):
np.random.seed(0)
X = np.random.rand(300, 5)
y = (X[:, 0] + X[:, 1] > 1.0).astype(int)
tf.keras.utils.set_random_seed(7) # 가중치 초기화까지 고정
m = Sequential([Input(shape=(5,)), Dense(8, activation='relu'),
Dense(1, activation='sigmoid')])
m.compile(optimizer='adam', loss='binary_crossentropy', metrics=metrics)
h = m.fit(X, y, epochs=5, batch_size=32, shuffle=False, verbose=0)
return h.history['loss']
a = train_with(['accuracy'])
b = train_with(['mae'])
print("metrics=['accuracy'] 손실:", [round(v, 6) for v in a])
print("metrics=['mae'] 손실:", [round(v, 6) for v in b])
print("두 손실 수열이 완전히 동일한가:", a == b)
metrics=['accuracy'] 손실: [0.730719, 0.722522, 0.714187, 0.705478, 0.696704]
metrics=['mae'] 손실: [0.730719, 0.722522, 0.714187, 0.705478, 0.696704]
두 손실 수열이 완전히 동일한가: True
선택지 5 실증 (loss='accuracy')
mx = Sequential([Input(shape=(5,)), Dense(1, activation='sigmoid')])
mx.compile(optimizer='adam', loss='accuracy', metrics=['mse'])
print("compile() 자체는 통과함")
try:
mx.fit(np.random.rand(40, 5), np.random.randint(0, 2, 40), epochs=1, verbose=0)
except Exception as e:
print("fit ->", type(e).__name__, ":", str(e).split("\n")[0])
compile() 자체는 통과함
fit -> ValueError : Could not interpret loss identifier: accuracy
왜 이렇게 푸는가
loss와 metrics는 이름이 나란히 붙어 있어 대등해 보이지만 역할이 전혀 다릅니다.
loss는 모델이 실제로 줄이는 대상입니다. 역전파는loss를 미분한 값으로 가중치를 고칩니다.metrics는 계기판입니다. 매 epoch 끝에 값을 계산해 화면과history에 얹어 줄 뿐, 가중치 갱신 계산에는 한 번도 등장하지 않습니다.
정확도가 loss가 될 수 없는 이유도 여기 있습니다. 정확도는 "맞았다/틀렸다"를 세는 계단 모양의 값이라 대부분의 지점에서 기울기가 0입니다. 기울기가 0이면 가중치를 어느 쪽으로 고쳐야 할지 알 수 없습니다. 그래서 Keras는 'accuracy'를 손실 함수 이름으로 해석하지 못하고 오류를 냅니다.
자주 틀리는 지점
loss와metrics의 자리를 바꿔 쓰는 것 —loss='accuracy', metrics=['binary_crossentropy']처럼 뒤집어 쓰는 답안이 실제로 나옵니다. "줄일 것은loss, 볼 것은metrics" 로 자리를 고정해 외우십시오.compile()이 통과했으니 맞다고 판단하는 것 — 위 실행 결과에서 보듯compile()은 문자열을 받아 두기만 하고, 실제 해석은fit()이 시작될 때 일어납니다. 오류가 나는 줄과 원인이 있는 줄이 다르다는 점이 이 함정의 핵심입니다.- 회귀에
metrics=['accuracy']를 그대로 두는 것. 오류는 안 나지만 연속값에 정확도는 의미가 없어 0에 가까운 숫자만 찍힙니다. 회귀 지표는['mae']입니다.
Q3 해답
정답 코드
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense
np.random.seed(42); tf.random.set_seed(42)
X_train = np.random.rand(1400, 10)
y_train = (X_train[:, 0] * 2 + X_train[:, 1] > 1.5).astype(int)
print('y_train[:5] =', y_train[:5], '/ 클래스:', np.unique(y_train))
model = Sequential()
model.add(Input(shape=(X_train.shape[1],)))
model.add(Dense(64, activation='relu'))
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='adam',
loss='binary_crossentropy', # (가)
metrics=['accuracy']) # (나)
history = model.fit(X_train, y_train,
epochs=50, batch_size=32,
validation_split=0.2, # (다)
verbose=1)
print('history 키:', sorted(history.history.keys()))
print('마지막 val_accuracy:', round(history.history['val_accuracy'][-1], 4))
y_train[:5] = [1 0 0 0 0] / 클래스: [0 1]
history 키: ['accuracy', 'loss', 'val_accuracy', 'val_loss']
마지막 val_accuracy: 0.9821
위
val_accuracy는 문제 지시대로epochs=50,batch_size=32,validation_split=0.2로 실행한 결과입니다. 환경과 seed에 따라 소수점 아래는 달라집니다.
history 키 4개: loss, accuracy, val_loss, val_accuracy
왜 이렇게 푸는가
- (가)
'binary_crossentropy'—print(y_train[:5])가[1 0 0 0 0],np.unique(y_train)이[0 1]이므로 클래스 2개짜리 이진분류입니다. 출력층이 이미Dense(1,'sigmoid')로 주어져 있으니 그 짝은binary_crossentropy하나뿐입니다. 출력층을 먼저 보고 loss를 맞추는 것도 유효한 검산법입니다. - (나)
['accuracy']— 분류 문제의 기본 지표입니다. 대괄호를 씌운 리스트로 넘긴다는 점에 주의하십시오. - (다)
validation_split=0.2— "학습 데이터의 20%를 검증 데이터로"라는 지시를 그대로 옮긴 인자입니다. 이 인자가 있어야history에val_키가 생기고, Ch06의EarlyStopping이 감시할 대상이 만들어집니다.
history 키 이름의 규칙은 단순합니다. loss는 항상 있고, 나머지 이름은 metrics에 넣은 문자열 그대로이며, 검증 데이터에서 잰 값에는 val_ 접두어가 붙습니다. 그래서 metrics=['mae']로 컴파일한 회귀 모델이라면 키는 loss, mae, val_loss, val_mae가 됩니다.
자주 틀리는 지점
metrics='accuracy'처럼 리스트를 빼는 것 — 최신 버전에서는 통과되기도 하지만 표준 표기는 리스트입니다. 지표를 두 개 이상 넣을 수 있는 자리이므로['accuracy']로 쓰는 습관을 들이십시오.validation_split을 빠뜨리는 것 — 오류는 나지 않습니다. 대신history키가['accuracy', 'loss']두 개뿐이 되어, 학습 곡선을 두 줄로 그리라는 다음 문항에서KeyError: 'val_loss'가 납니다. 한 문항의 누락이 다음 문항의 오류로 번지는 것이 실기 시험의 전형적인 실점 구조입니다.validation_split=0.2와validation_data=(...)를 동시에 쓰는 것. 둘 중 하나만 씁니다.epochs와batch_size를 문제 지시값과 다르게 적는 것. 지시된 숫자는 그대로 옮겨야 채점됩니다.
Q4 해답
정답 코드
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense
np.random.seed(42); tf.random.set_seed(42)
X_train = np.random.rand(2000, 12)
y_train = np.random.randint(0, 3, 2000)
print('y_train[:8] =', y_train[:8], '/ shape =', y_train.shape)
n_class = len(np.unique(y_train)) # (가)
model = Sequential()
model.add(Input(shape=(X_train.shape[1],)))
model.add(Dense(64, activation='relu'))
model.add(Dense(32, activation='relu'))
model.add(Dense(n_class, activation='softmax')) # (나)
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy', # (다)
metrics=['accuracy'])
history = model.fit(X_train, y_train,
epochs=60, batch_size=50,
validation_split=0.2, # (라)
verbose=1)
print('n_class =', n_class, '/ 출력 shape =', model.output_shape)
print('history 키:', sorted(history.history.keys()))
y_train[:8] = [1 1 1 0 0 1 2 2] / shape = (2000,)
n_class = 3 / 출력 shape = (None, 3)
history 키: ['accuracy', 'loss', 'val_accuracy', 'val_loss']
추가 1 — 1 epoch당 step 수: 32
학습에 쓰이는 행 수 = 2000 x (1 - 0.2) = 1600
1 epoch당 step 수 = ceil(1600 / 50) = 32
추가 2 — to_categorical로 바꾸는 경우: (다) 한 줄을 loss='categorical_crossentropy'로 고칩니다. (나)의 출력층은 그대로 둡니다.
from tensorflow.keras.utils import to_categorical
y_oh = to_categorical(y_train, n_class)
print('to_categorical 후 shape:', y_oh.shape, '/ 첫 행:', y_oh[0])
model_b = Sequential([Input(shape=(12,)), Dense(32, activation='relu'),
Dense(n_class, activation='softmax')]) # 출력층은 그대로
model_b.compile(optimizer='adam',
loss='categorical_crossentropy', # 이 줄만 교체
metrics=['accuracy'])
model_b.fit(X_train, y_oh, epochs=1, batch_size=50, verbose=0)
print('categorical_crossentropy + 원-핫 -> 학습 성공')
to_categorical 후 shape: (2000, 3) / 첫 행: [0. 1. 0.]
categorical_crossentropy + 원-핫 -> 학습 성공
왜 이렇게 푸는가
- (가)
len(np.unique(y_train))— 클래스 개수를 눈으로 세지 않습니다. 라벨이 1부터 시작하거나 중간 번호가 비어 있어도 이 코드는 정확한 개수를 돌려줍니다. - (나)
Dense(n_class, activation='softmax')— 다중분류 출력은 "각 클래스일 확률"의 목록이므로 노드 수가 클래스 수와 정확히 같아야 하고, softmax가 그 값들의 합을 1로 맞춰 줍니다. - (다)
'sparse_categorical_crossentropy'—y_train[:8]이[1 1 1 0 0 1 2 2], shape가(2000,)인 정수 라벨이기 때문입니다. 1차원이면sparse_입니다. - (라)
validation_split=0.2— Q3와 같습니다.
step 수 계산에서 놓치기 쉬운 것은 validation_split이 먼저 적용된다는 점입니다. 2,000행 전체가 아니라 검증분 20%를 뺀 1,600행만 가중치 갱신에 쓰이므로, ceil(2000/50) = 40이 아니라 ceil(1600/50) = 32가 정답입니다. verbose=1로 학습하면 진행 막대 왼쪽에 32/32가 찍히는 것으로 확인할 수 있습니다.
추가 2가 알려 주는 것은 출력층은 라벨 형태와 무관하다는 사실입니다. 정수 라벨이든 원-핫이든 다중분류 출력층은 언제나 Dense(K, 'softmax')이고, 바뀌는 것은 loss 한 줄뿐입니다.
자주 틀리는 지점
n_class = 3처럼 숫자를 손으로 적는 것 — 원-핫 인코딩으로 컬럼이 늘거나 이상치 행을 지우는 등 전처리 결과가 바뀌면 그대로 틀린 값이 됩니다.- step 수를
ceil(2000/50) = 40으로 답하는 것 — 검증분을 빼지 않은 실수입니다. to_categorical로 바꾸면서 출력층까지 건드리는 것 —Dense(1)등으로 바꾸면 오히려 shape 오류가 납니다. 고칠 곳은loss한 줄입니다.- 반대로 라벨은 정수 그대로 두고
loss만categorical_crossentropy로 바꾸는 실수 — 이것이 다음 Q5의 주제입니다.
Q5 해답
잘못된 줄: loss='categorical_crossentropy'
y_train은 [2 3 1 2 1 0]처럼 shape가 (600,)인 정수 라벨인데, categorical_crossentropy는 (600, 4) 형태의 원-핫 라벨을 기대합니다. 라벨 형태와 손실 함수가 어긋나 fit()에서 shape 오류가 납니다.
정답 코드
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense
from tensorflow.keras.utils import to_categorical
np.random.seed(42); tf.random.set_seed(42)
X_train = np.random.rand(600, 8)
y_train = np.digitize(X_train[:, 0] * 3 + X_train[:, 1], [0.9, 1.7, 2.5])
print('y_train[:6] =', y_train[:6], '/ shape =', y_train.shape)
# --- 잘못된 코드가 내는 오류 확인 ---
bad = Sequential([Input(shape=(8,)), Dense(32, activation='relu'),
Dense(4, activation='softmax')])
bad.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
try:
bad.fit(X_train, y_train, epochs=1, batch_size=32, validation_split=0.2, verbose=0)
except Exception as e:
print('[잘못]', type(e).__name__, '->', str(e).split("\n")[0])
# --- 수정 A: loss를 라벨에 맞춘다 (권장) ---
fix_a = Sequential([Input(shape=(8,)), Dense(32, activation='relu'),
Dense(4, activation='softmax')])
fix_a.compile(optimizer='adam',
loss='sparse_categorical_crossentropy', # 여기만 교체
metrics=['accuracy'])
fix_a.fit(X_train, y_train, epochs=20, batch_size=32, validation_split=0.2, verbose=0)
print('[수정 A] 성공 — y shape:', y_train.shape, '/ 출력 shape:', fix_a.output_shape)
# --- 수정 B: 라벨을 loss에 맞춘다 ---
y_oh = to_categorical(y_train, 4)
fix_b = Sequential([Input(shape=(8,)), Dense(32, activation='relu'),
Dense(4, activation='softmax')])
fix_b.compile(optimizer='adam',
loss='categorical_crossentropy', # loss는 그대로
metrics=['accuracy'])
fix_b.fit(X_train, y_oh, epochs=20, batch_size=32, validation_split=0.2, verbose=0)
print('[수정 B] 성공 — y shape:', y_oh.shape, '/ 출력 shape:', fix_b.output_shape)
y_train[:6] = [2 3 1 2 1 0] / shape = (600,)
[잘못] ValueError -> Arguments `target` and `output` must have the same rank (ndim). Received: target.shape=(32,), output.shape=(32, 4)
[수정 A] 성공 — y shape: (600,) / 출력 shape: (None, 4)
[수정 B] 성공 — y shape: (600, 4) / 출력 shape: (None, 4)
왜 이렇게 푸는가
오류 메시지가 원인을 그대로 말해 줍니다. target.shape=(32,)는 배치 32개의 정답이 숫자 하나씩이라는 뜻이고, output.shape=(32, 4)는 모델이 배치마다 확률 4개씩을 내놓았다는 뜻입니다. categorical_crossentropy는 이 둘의 차원 수가 같아야(둘 다 2차원이어야) 계산할 수 있습니다.
여기서 중요한 것은 어느 쪽을 고쳐도 된다는 점입니다.
- 수정 A — 라벨을 그대로 두고 loss에
sparse_를 붙여 "정답은 정수 하나로 들어온다"고 알려 줍니다. 손댈 곳이 한 줄뿐이라 시험에서는 이쪽이 안전합니다. - 수정 B —
to_categorical로 라벨을(600, 4)원-핫으로 펼쳐 loss가 기대하는 형태에 맞춥니다. 문제가 "원-핫 인코딩된 타깃을 사용하시오"라고 지시했다면 이쪽입니다.
출력층 Dense(4,'softmax')는 두 방법 모두에서 그대로라는 점을 다시 확인하십시오. 손댈 곳은 loss 또는 y 둘 중 하나입니다.
자주 틀리는 지점
- 오류가
fit()에서 나니까fit()의 인자를 의심하는 것 —epochs나batch_size를 아무리 바꿔도 해결되지 않습니다. shape 오류를 만나면print(y_train.shape)와model.output_shape를 나란히 찍어 보는 것이 가장 빠른 진단입니다. - 출력층 units를 라벨 개수에 맞춘다며
Dense(1)로 줄이는 것 — 오류 메시지가 사라지는 방향으로만 고치다 생기는 사고입니다. 클래스가 4개면 출력 노드는 4개여야 합니다. - 수정 A와 수정 B를 섞는 것 —
to_categorical로 라벨을 펼쳐 놓고 loss는sparse_categorical_crossentropy로 두면 같은 오류가 반대 방향으로 납니다. loss와 y는 반드시 한 쌍으로 함께 바꿉니다.
Q6 해답
잘못된 곳 2군데
model.add(Dense(1, activation='sigmoid'))— 회귀 출력층에 활성화 함수가 남아 있습니다.sigmoid는 출력을 0~1로 눌러 버리므로 88~278 범위의 값을 절대 맞힐 수 없습니다.Dense(1)로 고칩니다.loss='accuracy', metrics=['mse']—loss와metrics의 자리가 뒤바뀌었습니다.'accuracy'는 손실 함수가 아니어서fit()에서 오류가 납니다. 회귀 표준 조합인loss='mse', metrics=['mae']로 고칩니다.
정답 코드
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense
np.random.seed(42); tf.random.set_seed(42)
X_train = np.random.rand(800, 7)
y_train = (120 * X_train[:, 0] + 60 * X_train[:, 1] + 30 * X_train[:, 2]
+ 80 + np.random.randn(800) * 5)
print('y_train[:4] =', np.round(y_train[:4], 1),
'/ 범위:', round(y_train.min(), 1), '~', round(y_train.max(), 1))
# --- 오류 2 확인: 'accuracy'는 손실 함수가 아니다 ---
tmp = Sequential([Input(shape=(7,)), Dense(1, activation='sigmoid')])
tmp.compile(optimizer='adam', loss='accuracy', metrics=['mse'])
print("compile() 자체는 통과함 (여기서는 오류가 나지 않는다)")
try:
tmp.fit(X_train, y_train, epochs=1, batch_size=32, verbose=0)
except Exception as e:
print('fit ->', type(e).__name__, ':', str(e).split("\n")[0])
# --- 오류 1 확인: loss만 고치고 sigmoid를 남겨 두면 ---
tf.keras.utils.set_random_seed(42)
half = Sequential([Input(shape=(7,)), Dense(32, activation='relu'),
Dense(1, activation='sigmoid')]) # sigmoid 잔존
half.compile(optimizer='adam', loss='mse', metrics=['mae'])
hh = half.fit(X_train, y_train, epochs=100, batch_size=32,
validation_split=0.2, verbose=0)
print('sigmoid 잔존 — 마지막 val_mae =', round(hh.history['val_mae'][-1], 2))
# --- 수정 완료본 ---
tf.keras.utils.set_random_seed(42)
model = Sequential()
model.add(Input(shape=(X_train.shape[1],)))
model.add(Dense(32, activation='relu'))
model.add(Dense(1)) # 수정 1: 활성화 없음
model.compile(optimizer='adam',
loss='mse', # 수정 2: 손실 함수
metrics=['mae']) # 수정 2: 지표
history = model.fit(X_train, y_train,
epochs=100, batch_size=32,
validation_split=0.2, verbose=0)
print('수정본 — 마지막 val_mae =', round(history.history['val_mae'][-1], 2))
print('history 키:', sorted(history.history.keys()))
y_train[:4] = [199.4 235.6 123.7 121. ] / 범위: 88.8 ~ 277.9
compile() 자체는 통과함 (여기서는 오류가 나지 않는다)
fit -> ValueError : Could not interpret loss identifier: accuracy
sigmoid 잔존 — 마지막 val_mae = 186.05
수정본 — 마지막 val_mae = 24.02
위
val_mae값은 문제 지시대로epochs=100으로 실행한 결과이며, 환경에 따라 소수점 아래는 달라집니다.epochs를 300으로 늘리면 수정본은 4 부근까지 내려가지만sigmoid가 남은 모델은 186.05에서 전혀 움직이지 않습니다.
왜 이렇게 푸는가
sigmoid가 남은 모델의 val_mae가 186 근처에서 한 발짝도 움직이지 않는다는 것이 이 문제의 핵심입니다.
출력이 0~1 사이로 강제되니 모델이 낼 수 있는 최선의 답은 언제나 1.0이고, 정답의 평균은 약 186입니다. 즉 평균적으로 185 정도씩 틀리는 상태에서 학습이 멈춰 있습니다. 가중치를 아무리 고쳐도 출력 범위 자체가 막혀 있으므로 손실이 줄어들 길이 없습니다.
이 오류는 오류 메시지를 내지 않습니다. 학습은 끝까지 정상적으로 돌아가고, history도 멀쩡히 만들어집니다. 발견할 수 있는 유일한 단서는 mae가 비정상적으로 크고 전혀 줄지 않는다는 것뿐입니다. 그래서 회귀 문항에서는 학습 후 mae를 타깃의 크기와 비교해 보는 습관이 필요합니다. 타깃 평균이 186인데 mae가 186이면 모델이 아무것도 배우지 못한 것입니다.
두 번째 오류는 Q2에서 본 자리 바꿈입니다. 회귀의 표준 조합은 loss='mse' + metrics=['mae'] 입니다. mse는 오차를 제곱하므로 큰 오차에 민감해 학습 신호가 잘 전달되고, mae는 "평균 몇 만큼 틀렸는가"를 사람이 읽기 쉬운 단위로 보여 줍니다.
자주 틀리는 지점
- 분류 코드를 복사해 회귀로 고치면서
loss만 바꾸는 것 — 이 챕터가 경고하는 대표 사고입니다. 유형을 바꿀 때는 출력층·loss·metrics 세 줄을 한 묶음으로 고쳐야 합니다. - 회귀 출력층에
relu를 붙이는 것 —sigmoid만큼 치명적이지는 않지만 음수를 낼 수 없게 되므로, 타깃에 음수가 섞인 문제에서는 같은 방식으로 막힙니다. 회귀 출력층은 활성화 함수를 아예 적지 않는 것이 정답입니다. metrics=['accuracy']를 회귀에 그대로 두는 것 — 오류는 없지만 0에 가까운 무의미한 값만 찍힙니다.- 오류를 하나만 찾고 넘어가는 것.
loss='accuracy'는 실행이 멈추니 금방 보이지만, 출력층의sigmoid는 조용히 살아남습니다. 실행이 되기 시작했다고 해서 다 고친 것이 아닙니다.