AICE Associate 딥러닝 실전 교본 — 도해로 익히고 코드로 끝낸다목차

Part II. 딥러닝의 뼈대 · Chapter 4

Keras로 모델 만들기

이 챕터에서 배우는 것
Ch03에서 손으로 계산했던 뉴런과 층을, 이제 Keras 코드 몇 줄로 쌓습니다. 이 챕터는 모델을 만드는 것(구성)까지입니다. 학습에 필요한 손실 함수와 optimizer 설정은 Ch05에서 다룹니다.
시험 연관성: 실기의 딥러닝 문항은 거의 예외 없이 "은닉층 2개 이상의 모델을 만드시오"로 시작합니다. 이 챕터의 코드 골격이 그 문장의 정답입니다.


4.1 모델 만들기는 전체 4단계 중 첫 단계입니다

Keras 모델 제작 4단계 파이프라인. 구성, 컴파일, 학습, 평가 네 단계 중 이 챕터는 첫 단계인 구성을 다룹니다. Keras 모델 제작 4단계 파란 칸이 이 챕터의 범위 구성 Sequential Dense Ch04 (이 챕터) 컴파일 optimizer loss Ch05 학습 fit() Ch05 평가 evaluate() predict() Ch07 순서를 건너뛸 수 없다. compile() 없이 fit()을 부르면 오류가 난다.
그림 4-1Keras 모델 제작 4단계

Keras로 딥러닝 모델을 다루는 일은 항상 같은 순서를 밟습니다. 구성 → 컴파일 → 학습 → 평가, 네 단계입니다.

이 순서는 Ch02에서 본 머신러닝의 3줄 패턴(생성 → fit → predict)과 대응됩니다. 다른 점은 sklearn이 한 줄로 끝내는 "모델 생성"이 Keras에서는 구성과 컴파일 두 단계로 나뉜다는 것입니다. 층을 어떻게 쌓을지(구성)와 그 층들을 어떤 기준으로 학습시킬지(컴파일)를 따로 정하기 때문입니다.

이 챕터가 책임지는 것은 첫 번째 칸, 구성입니다. 구성 단계에서 결정하는 것은 세 가지입니다.

  1. 입력을 몇 개 받을 것인가 (입력 shape)
  2. 은닉층을 몇 층, 몇 개 노드로 쌓을 것인가
  3. 출력을 몇 개, 어떤 활성화 함수(activation function)로 낼 것인가

⚠️ 시험 포인트
문제가 "모델을 만들고 학습시키시오"라고 한 문장으로 지시해도, 답안은 구성 → 컴파일 → 학습 세 덩어리를 모두 써야 합니다. 구성만 하고 compile()을 빠뜨리면 fit()에서 오류가 납니다.


4.2 Sequential — 코드 한 줄이 층 하나입니다

Sequential 모델에서 model.add 코드 한 줄이 층 하나에 대응하는 모습. 작성한 순서가 데이터가 지나가는 순서가 됩니다. 코드 한 줄 = 층 하나 작성 순서 데이터가 지나가는 순서 model.add(Input(shape=(8,))) model.add(Dense(64, activation='relu')) model.add(Dense(32, activation='relu')) model.add(Dense(1, activation='sigmoid')) 입력 — 특성 8개 은닉층 1 — 노드 64, relu 은닉층 2 — 노드 32, relu 출력층 — 노드 1, sigmoid Input은 층으로 세지 않는다: len(model.layers) = 3
그림 4-2코드 한 줄과 층 하나의 대응

Sequential은 층을 위에서 아래로 한 줄로 쌓는 모델 컨테이너입니다. model.add()를 부른 순서가 그대로 데이터가 지나가는 순서가 됩니다.

Dense는 Ch03에서 본 완전연결층입니다. 앞 층의 모든 노드가 다음 층의 모든 노드에 연결됩니다. 인자는 두 개만 기억하면 됩니다.

인자 의미 실기에서 쓰는 값
units (첫 번째 위치 인자) 이 층의 노드 개수 은닉층은 보통 32, 64, 128
activation 노드 출력에 씌우는 활성화 함수 은닉층은 'relu'

가장 기본이 되는 모델을 만들어 보겠습니다.

입력 8개를 받아 은닉층 2개를 거쳐 값 하나를 내는 모델을 구성하는 코드입니다.

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense

tf.random.set_seed(42)

n_features = 8

model = Sequential()
model.add(Input(shape=(n_features,)))        # 입력 정의
model.add(Dense(64, activation='relu'))      # 은닉층 1
model.add(Dense(32, activation='relu'))      # 은닉층 2
model.add(Dense(1, activation='sigmoid'))    # 출력층

print(len(model.layers))

출력은 3입니다. Input은 실제 연산을 하는 층이 아니라 입력 형태 선언이므로 층 목록에 세지 않습니다. Dense 세 개만 층으로 잡힙니다.

리스트로 한 번에 넘기는 표기도 같은 결과를 냅니다. 취향에 따라 쓰되, 시험장에서는 한 줄씩 추가하는 add() 방식이 중간에 층을 넣고 빼기 편합니다.

model = Sequential([
    Input(shape=(n_features,)),
    Dense(64, activation='relu'),
    Dense(32, activation='relu'),
    Dense(1, activation='sigmoid'),
])

은닉층을 몇 개 쌓아야 할지 고민된다면, 실기 기준으로는 은닉층 2개, 노드 수는 앞 층이 크고 뒤 층이 작아지는 형태(64 → 32)를 기본값으로 삼으십시오. Ch03의 ch03_width-depth에서 봤듯 층과 노드를 무작정 늘리면 표현력은 커지지만 과적합 위험도 함께 커집니다.

⚠️ 시험 포인트
"은닉층 2개 이상"이라는 지시에서 은닉층은 입력층과 출력층을 뺀 중간층입니다. Dense(64) + Dense(1, 'sigmoid')는 은닉층이 1개뿐이라 조건을 채우지 못합니다. 출력층은 은닉층 개수에 포함되지 않습니다.


4.3 입력 shape — 샘플 수는 넣지 않습니다

입력 shape를 정하는 규칙. 표 데이터의 열 수(특성 수)만 Input의 shape에 넣고, 행 수(샘플 수)는 넣지 않습니다. 입력 shape는 열 수만 쓴다 8열 = 특성 수 1,000행 (샘플 수) Input(shape=(8,)) 8 = X_train_scaled.shape[1] 행 수(샘플 수)는 shape에 넣지 않는다 Input(shape=(1000, 8)) 처럼 쓰면 틀린 표기
그림 4-3입력 shape를 정하는 규칙

Input(shape=(n,))의 n은 특성(컬럼)의 개수입니다. 데이터가 1,000행 8열이라면 shape=(8,)입니다. 행 수는 절대 들어가지 않습니다.

모델은 "샘플 하나가 어떻게 생겼는가"만 알면 되고, 샘플이 몇 개 들어올지는 학습할 때 알아서 처리하기 때문입니다.

전처리를 마친 데이터에서 입력 크기를 직접 뽑아내는 코드입니다.

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

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)

print(X_train_scaled.shape)          # (398, 30)
n_features = X_train_scaled.shape[1] # 30
print(n_features)

X_train_scaled.shape는 (398, 30), 즉 (샘플 수, 특성 수)입니다. 여기서 필요한 것은 뒤의 30뿐이므로 X_train_scaled.shape[1]로 꺼냅니다. 숫자를 직접 세어 적지 말고 항상 이 방식으로 뽑으십시오. 전처리 단계에서 원-핫 인코딩으로 컬럼이 늘어나거나 불필요한 컬럼을 지우면 특성 수가 바뀌는데, 손으로 적은 숫자는 그때 틀립니다.

shape=(30,)의 쉼표도 빠뜨리면 안 됩니다. 파이썬에서 (30)은 그냥 정수 30이고, 원소가 하나인 튜플은 (30,)으로 써야 합니다.

구버전 표기 참고. 오래된 코드에서는 첫 Dense에 입력 크기를 붙인 표기를 자주 봅니다.

# 구버전 표기 — 읽을 줄만 알면 됩니다. 새로 쓸 때는 Input을 쓰십시오.
model.add(Dense(64, activation='relu', input_shape=(n_features,)))

이 표기는 Keras 3에서 권장되지 않습니다. 이 교재는 Keras 2와 3 양쪽에서 동작하는 model.add(Input(shape=(n,)))를 표준으로 씁니다.

⚠️ 시험 포인트
Input(shape=(1000, 8))처럼 샘플 수를 넣는 것이 가장 흔한 실수입니다. shape에 들어가는 것은 샘플 한 개의 모양이며, 표 형태 데이터에서는 언제나 (특성 수,) 하나짜리 튜플입니다.


4.4 출력층 — 문제 유형이 units와 activation을 결정합니다

문제 유형별 출력층 설계 분기도. 이진분류, 다중분류, 회귀에 따라 출력층의 units와 activation이 하나로 정해집니다. 문제 유형이 출력층을 정한다 문제 유형? 이진분류 (0 / 1) units 1 activation 'sigmoid' 1일 확률 하나 (0~1) Dense(1, activation='sigmoid') 손실 함수는 Ch05 참조 다중분류 (K개 클래스) units K activation 'softmax' 클래스별 확률 K개 (합 = 1) Dense(K, activation='softmax') 손실 함수는 Ch05 참조 회귀 (연속값) units 1 activation 쓰지 않음 예측한 숫자 그 자체 Dense(1) 손실 함수는 Ch05 참조
그림 4-4문제 유형별 출력층 설계

은닉층은 상황에 따라 조금 늘리거나 줄여도 점수에 큰 영향이 없습니다. 그러나 출력층은 문제 유형에 따라 정답이 하나로 정해집니다. 여기가 틀리면 모델이 아예 학습되지 않거나, 학습되더라도 결과를 해석할 수 없습니다.

문제 유형 출력층 코드 출력의 의미
이진분류 (0/1) Dense(1, activation='sigmoid') 1일 확률 하나 (0~1)
다중분류 (K개 클래스) Dense(K, activation='softmax') 클래스별 확률 K개 (합 = 1)
회귀 (연속값 예측) Dense(1) — 활성화 지정 없음 예측한 숫자 그 자체

세 가지 모두 Ch03의 ch03_activation-shapes에서 본 활성화 함수의 출력 범위로 설명됩니다. sigmoid는 0~1 사이 하나를 내므로 "그럴 확률"에 맞고, softmax는 여러 값을 합이 1인 확률 분포로 만들어 주므로 "여러 클래스 중 하나"에 맞습니다. 회귀는 예측값이 음수일 수도, 수백만일 수도 있으므로 값을 좁히는 활성화 함수를 씌우면 안 됩니다.

세 유형의 출력층만 바꿔 보는 코드입니다. 앞의 은닉층 부분은 셋 다 동일합니다.

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense


def build_hidden(n_features):
    m = Sequential()
    m.add(Input(shape=(n_features,)))
    m.add(Dense(64, activation='relu'))
    m.add(Dense(32, activation='relu'))
    return m


# (1) 이진분류
binary_model = build_hidden(30)
binary_model.add(Dense(1, activation='sigmoid'))

# (2) 다중분류 — 클래스 개수를 데이터에서 뽑습니다
y_multi = np.array([0, 1, 2, 1, 0, 2, 2])
n_class = len(np.unique(y_multi))          # 3
multi_model = build_hidden(30)
multi_model.add(Dense(n_class, activation='softmax'))

# (3) 회귀 — activation을 쓰지 않습니다
reg_model = build_hidden(30)
reg_model.add(Dense(1))

print(n_class)                              # 3

다중분류의 K도 손으로 세지 말고 len(np.unique(y))로 뽑는 습관을 들이십시오. 클래스가 3개인데 Dense(2, activation='softmax')로 쓰면 학습 단계에서 라벨과 출력 개수가 맞지 않아 오류가 납니다.

📎 손실 함수는 Ch05에서
출력층 옆에는 항상 짝이 되는 손실 함수가 붙습니다. 다만 손실 함수 선택 규칙은 라벨이 정수인지 원-핫인지까지 따져야 해서 분기가 한 겹 더 깊습니다. 그 정본은 Ch05의 ch05_loss-decision 도해입니다. 이 챕터에서는 "출력층은 이렇게 생겼다"까지만 확실히 익히고, 짝짓기는 Ch05에서 한 번에 정리하십시오.

⚠️ 시험 포인트
회귀 문제에 Dense(1, activation='sigmoid')를 쓰는 실수가 자주 나옵니다. sigmoid는 출력을 0~1로 눌러 버리므로, 집값이나 잔여 수명 같은 값은 영원히 맞힐 수 없습니다. 회귀 출력층에는 활성화 함수를 쓰지 않습니다.


4.5 summary() — 만든 모델을 눈으로 확인합니다

파라미터 수가 계산되는 방식. 입력 4노드와 은닉 5노드의 연결에서 가중치 20개와 편향 5개가 나와 25가 되고, 이 값이 summary 표의 Param 열에 나타납니다. 파라미터 수 세는 법 입력 4노드 Dense(5) 5노드 첫 층 dense (Dense)의 파라미터 파라미터 수 = (앞 층 노드 x 이 층 노드) + 이 층 노드 (4 x 5) + 5 = 25 가중치 4 x 5 = 20개 + 편향 5개 Layer (type) Output Shape Param # dense (Dense) (None, 5) 25 dense_1 (Dense) (None, 1) 6 Total params: 31 둘째 층도 같은 식으로 (5 x 1) + 1 = 6, 두 층을 더해 Total params 31
그림 4-5파라미터 수가 계산되는 방식

model.summary()는 지금까지 쌓은 층을 표로 보여줍니다. 학습을 시작하기 전에 의도한 대로 만들어졌는지 확인하는 유일한 수단이므로, 구성 직후에 반드시 한 번 호출하십시오.

입력 4개 → 은닉 5개 → 출력 1개짜리 최소 모델로 표를 읽어 보겠습니다.

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense

demo = Sequential()
demo.add(Input(shape=(4,)))
demo.add(Dense(5, activation='relu'))
demo.add(Dense(1, activation='sigmoid'))
demo.summary()

출력의 요지는 다음과 같습니다(표 서식은 Keras 버전에 따라 조금 다르게 보일 수 있습니다).

Layer (type)              Output Shape        Param #
=====================================================
dense (Dense)             (None, 5)                25
dense_1 (Dense)           (None, 1)                 6
=====================================================
Total params: 31
Trainable params: 31
Non-trainable params: 0

표에서 읽어야 할 것은 두 열입니다.

Output Shape의 None — 앞자리 None은 "샘플 수는 아직 정해지지 않았다"는 뜻입니다. 4.3에서 입력 shape에 행 수를 넣지 않은 결과가 여기 나타납니다. 오류가 아니므로 놀랄 필요가 없습니다. 뒷자리 숫자가 그 층의 units와 같은지만 확인하면 됩니다.

Param # — 그 층이 학습으로 조정하는 값(가중치와 편향)의 개수입니다. 계산식은 언제나 같습니다.

파라미터 수 = (앞 층 노드 수 × 이 층 노드 수) + 이 층 노드 수

앞부분은 가중치, 뒤에 더하는 것은 노드마다 하나씩 있는 편향입니다. 첫 층은 (4 × 5) + 5 = 25, 둘째 층은 (5 × 1) + 1 = 6이 되어 합이 31입니다. Ch03의 ch03_neuron에서 뉴런 하나가 w1*x1 + ... + b를 계산했던 그 w와 b의 총 개수입니다.

Total params가 0이거나 summary()가 "모델이 아직 build되지 않았다"는 메시지를 낸다면, Input을 빠뜨렸다는 신호입니다. 입력 크기를 모르면 Keras가 가중치 행렬 크기를 정할 수 없기 때문입니다.


4.6 Dropout과 BatchNormalization — 위치가 전부입니다

Dropout과 BatchNormalization을 넣는 위치. 은닉층 Dense 뒤에 BatchNormalization, 그 뒤에 Dropout을 두며 출력층 뒤에는 넣지 않습니다. 보조 층을 넣는 위치 은닉층 블록 Dense 뒤에 BN, 그 뒤에 Dropout Input(shape=(30,)) Dense(64, activation='relu') BatchNormalization() 값의 분포를 정돈 Dropout(0.3) 노드 일부를 끔 0.3 = 끄는 비율 Dense(32, activation='relu') Dropout(0.3) Dense(1, activation='sigmoid') 출력층 Dropout(0.3) 출력층 뒤에는 넣지 않는다 보조 층은 은닉층 Dense 뒤에만 넣는다. 순서는 Dense, BatchNormalization, Dropout.
그림 4-6Dropout과 BatchNormalization을 넣는 위치

두 층은 예측값을 직접 만들지 않고 학습이 더 잘 되도록 돕는 보조 층입니다. 그래서 인자보다 어디에 넣느냐가 중요합니다.

층 하는 일 넣는 위치
Dropout(rate) 학습 중 노드 일부를 임시로 꺼서 과적합을 줄임 은닉층 Dense 뒤
BatchNormalization() 층을 지나는 값의 분포를 정돈해 학습을 안정시킴 은닉층 Dense 뒤, Dropout 앞

Dropout(0.3)의 0.3은 끄는 비율입니다. 남기는 비율이 아닙니다. 실기에서는 0.2~0.5 범위를 쓰고, 지시가 없으면 0.3 정도가 무난합니다. Dropout이 학습할 때와 예측할 때 어떻게 다르게 동작하는지는 Ch06의 ch06_dropout-mechanism에서 자세히 다룹니다.

보조 층을 넣은 표준 배치입니다.

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense, Dropout, BatchNormalization

model = Sequential()
model.add(Input(shape=(30,)))
model.add(Dense(64, activation='relu'))
model.add(BatchNormalization())
model.add(Dropout(0.3))
model.add(Dense(32, activation='relu'))
model.add(Dropout(0.3))
model.add(Dense(1, activation='sigmoid'))
model.summary()

BatchNormalization은 summary()에서 특이한 모습을 보입니다. 파라미터가 노드 수의 4배(여기서는 64 × 4 = 256)로 잡히고, 그중 절반이 Non-trainable params로 분류됩니다. 학습으로 조정하는 값 두 종류와, 학습 중 관찰한 통계를 저장해 두는 값 두 종류가 함께 있기 때문입니다. 오류가 아니니 그대로 두면 됩니다.

⚠️ 시험 포인트
출력층 뒤에는 Dropout을 넣지 않습니다. 출력층 뒤에 Dropout을 두면 예측 확률 자체가 무작위로 꺼져 결과가 망가집니다. 마지막 Dense는 언제나 모델의 맨 아래에 있어야 합니다.


시험장 표준 코드

전처리를 마친 데이터에서 모델 구성까지 이어지는, 그대로 옮겨 쓸 수 있는 완결 코드입니다. 이진분류 기준이며, 유형이 바뀌면 마지막 Dense 한 줄만 4.4의 표대로 교체하십시오.

import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense, Dropout, BatchNormalization
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)

# 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)

n_features = X_train_scaled.shape[1]
print('입력 특성 수:', n_features)

# 2) 모델 구성 (이 챕터)
model = Sequential()
model.add(Input(shape=(n_features,)))
model.add(Dense(64, activation='relu'))
model.add(BatchNormalization())
model.add(Dropout(0.3))
model.add(Dense(32, activation='relu'))
model.add(Dropout(0.3))
model.add(Dense(1, activation='sigmoid'))   # ← 유형별 교체 지점

model.summary()

# 3) 구성한 모델 저장 (선택)
model.save('my_model.keras')                # 구버전 표기: 'my_model.h5'

예상 출력 요지입니다.

입력 특성 수: 30

Layer (type)                     Output Shape       Param #
===========================================================
dense (Dense)                    (None, 64)            1984
batch_normalization (BatchNor…)  (None, 64)             256
dropout (Dropout)                (None, 64)               0
dense_1 (Dense)                  (None, 32)            2080
dropout_1 (Dropout)              (None, 32)               0
dense_2 (Dense)                  (None, 1)                33
===========================================================
Total params: 4,353
Trainable params: 4,225
Non-trainable params: 128

Dropout의 Param #가 0인 것은 정상입니다. 노드를 끄기만 할 뿐 학습하는 값이 없기 때문입니다.

여기까지가 이 챕터의 범위입니다. 이 모델은 아직 학습할 수 없습니다. 어떤 기준으로 오차를 재고 가중치를 갱신할지 정하지 않았기 때문입니다. 다음 챕터에서 compile()과 fit()을 붙여 이 모델을 실제로 학습시킵니다.


핵심 요약

항목 기억할 것
4단계 구성 → 컴파일(Ch05) → 학습(Ch05) → 평가(Ch07)
입력 model.add(Input(shape=(n_features,))), n_features = X_train_scaled.shape[1], 샘플 수는 넣지 않음
은닉층 Dense(64, activation='relu') → Dense(32, activation='relu'), 은닉층 2개 이상
출력층 이진 Dense(1,'sigmoid') / 다중 Dense(K,'softmax') / 회귀 Dense(1)
보조 층 Dense 뒤에 BatchNormalization() → Dropout(0.3) 순서, 출력층 뒤에는 넣지 않음
확인 model.summary()로 Output Shape와 Param #를 눈으로 검증

파라미터 수 계산식: (앞 층 노드 수 × 이 층 노드 수) + 이 층 노드 수

Ch04 확인 문제 — Keras로 모델 만들기

본 문제는 공개된 출제 기준에 따른 유형 연습 문제입니다. 기출 원문이 아닙니다.

이 챕터의 범위는 모델 구성까지입니다. compile()·fit()은 Ch05, 평가는 Ch07에서 다룹니다.
따라서 모든 문항은 Sequential / Input / Dense / Dropout / BatchNormalization / summary()만으로 풀립니다.

총 6문항 (객관식 2 / 코드 빈칸 2 / 오류 찾기 2), 권장 소요 시간 20분.
공통 import는 다음과 같다고 가정합니다.

python import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Input, Dense, Dropout, BatchNormalization


문제

Q1. (객관식 · 기본) 입력 정의

전처리를 모두 마친 X_train의 shape가 (1200, 12)입니다.
이 데이터로 학습할 모델의 입력 정의로 옳은 것은?

  1. model.add(Input(shape=(1200, 12)))
  2. model.add(Input(shape=(1200,)))
  3. model.add(Input(shape=(12,)))
  4. model.add(Input(shape=(12)))
  5. model.add(Input(shape=(12, 1200)))

Q2. (객관식 · 기본) Param # 읽기

다음과 같이 구성한 모델의 model.summary()가 출력하는 Total params는 얼마입니까?

model = Sequential()
model.add(Input(shape=(10,)))
model.add(Dense(16, activation='relu'))
model.add(Dense(8, activation='relu'))
model.add(Dense(3, activation='softmax'))
model.summary()
  1. 27
  2. 208
  3. 312
  4. 339
  5. 360

Q3. (코드 빈칸 · 기본) 이진분류 모델 구성

전처리를 마친 X_tr의 shape가 (800, 15)이고, 타깃은 0 또는 1인 이진분류 문제입니다.
은닉층 2개(노드 64 → 32, 활성화 함수는 relu)를 두는 모델을 구성하십시오.

(가)에는 특성 수를 데이터에서 뽑아내는 코드를, (나)에는 입력 정의 한 줄을, (다)에는 출력층 한 줄을 채우십시오.

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense

X_tr = np.zeros((800, 15))          # 전처리를 마친 학습 데이터라고 가정합니다

n_features = ______(가)______

model = Sequential()
model.add(______(나)______)
model.add(Dense(64, activation='relu'))
model.add(Dense(32, activation='relu'))
model.add(______(다)______)

model.summary()

추가로, 이 모델의 Total params를 계산식과 함께 쓰십시오.


Q4. (코드 빈칸 · 응용) 다중분류 모델 + 보조 층

y_train은 정수 라벨이고, X_tr의 특성 수는 20입니다.
첫 은닉층 뒤에 BatchNormalization → Dropout(0.3) 순서로 보조 층을 넣고, 두 번째 은닉층 뒤에도 Dropout(0.3)을 넣는 다중분류 모델을 구성하십시오.

(가)에는 클래스 개수를 데이터에서 뽑아내는 코드를, (나)·(다)에는 보조 층 두 줄을, (라)에는 출력층 한 줄을 채우십시오.

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense, Dropout, BatchNormalization

X_tr = np.zeros((10, 20))
y_train = np.array([0, 2, 1, 3, 2, 0, 1, 3, 3, 1])

n_features = X_tr.shape[1]
n_class = ______(가)______

model = Sequential()
model.add(Input(shape=(n_features,)))
model.add(Dense(64, activation='relu'))
model.add(______(나)______)
model.add(______(다)______)
model.add(Dense(32, activation='relu'))
model.add(Dropout(0.3))
model.add(______(라)______)

model.summary()

추가로, summary()에서 BatchNormalization 층의 Param #가 몇으로 찍히는지와, 그중 일부가 Non-trainable params로 분류되는 이유를 한 줄로 쓰십시오.


Q5. (오류 찾기 · 기본) 입력 정의가 잘못된 모델

아래 코드는 오류 메시지 없이 실행되지만, summary()의 출력 shape가 의도와 다릅니다.
잘못된 줄 한 곳을 찾아 지적하고, 올바르게 고친 코드를 쓰십시오.

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense

X_train = np.zeros((1000, 8))       # 샘플 1000개, 특성 8개

model = Sequential()
model.add(Input(shape=X_train.shape))
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))

model.summary()
print(model.output_shape)

Q6. (오류 찾기 · 응용) 다중분류 모델의 두 가지 오류

y_train에는 0, 1, 2, 3 네 종류의 클래스가 들어 있습니다.
아래 모델 구성 코드에는 잘못된 곳이 두 군데 있습니다. 각각을 지적하고 수정한 전체 코드를 쓰십시오.

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense, Dropout

y_train = np.array([0, 1, 2, 3, 1, 0, 3, 2])

model = Sequential()
model.add(Input(shape=(20,)))
model.add(Dense(64, activation='relu'))
model.add(Dropout(0.3))
model.add(Dense(32, activation='relu'))
model.add(Dense(2, activation='softmax'))
model.add(Dropout(0.3))

model.summary()

해답 및 해설 보기

Q1 해답

정답: 3번 model.add(Input(shape=(12,)))

정답 코드 (실증 예제)

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense

X_train = np.zeros((1200, 12))
n_features = X_train.shape[1]        # 12

model = Sequential()
model.add(Input(shape=(n_features,)))
model.add(Dense(32, activation='relu'))
print("X_train.shape :", X_train.shape)
print("n_features    :", n_features)
print("출력 shape    :", model.output_shape)
X_train.shape : (1200, 12)
n_features    : 12
출력 shape    : (None, 32)

왜 이렇게 푸는가

Input(shape=...)에 넣는 것은 샘플 한 개의 모양입니다. 표 형태 데이터에서 샘플 한 개는 컬럼 12개짜리 한 줄이므로 (12,)입니다. 샘플이 몇 개 들어올지는 학습할 때 정해지므로 모델은 알 필요가 없고, 그 자리는 summary()에서 None으로 표시됩니다.

숫자 12를 손으로 적지 말고 X_train.shape[1]로 뽑는 습관이 중요합니다. 원-핫 인코딩으로 컬럼이 늘거나 불필요한 컬럼을 지우면 특성 수가 바뀌는데, 손으로 적은 숫자는 그때 그대로 틀린 값이 됩니다.

자주 틀리는 지점

  • 1번·2번처럼 샘플 수(1200)를 넣는 것 — 이 챕터에서 가장 흔한 실수입니다. shape에 행 수는 절대 들어가지 않습니다.
  • 4번 shape=(12) — 괄호만 쳤을 뿐 파이썬에서는 그냥 정수 12입니다. 원소가 하나인 튜플은 반드시 쉼표를 붙여 (12,)로 씁니다. 실제로 실행하면 ValueError: Cannot convert '12' to a shape.가 납니다.
  • 5번처럼 순서를 뒤집는 것도 같은 오해에서 나옵니다. 정답은 언제나 (특성 수,) 하나짜리 튜플입니다.

Q2 해답

정답: 4번 (339)

정답 코드 (실증 예제)

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense

model = Sequential()
model.add(Input(shape=(10,)))
model.add(Dense(16, activation='relu'))
model.add(Dense(8, activation='relu'))
model.add(Dense(3, activation='softmax'))
model.summary()

hand = (10 * 16 + 16) + (16 * 8 + 8) + (8 * 3 + 3)
print("손 계산 :", (10 * 16 + 16), "+", (16 * 8 + 8), "+", (8 * 3 + 3), "=", hand)
print("실제 값 :", model.count_params())
Layer (type)              Output Shape        Param #
=====================================================
dense (Dense)             (None, 16)              176
dense_1 (Dense)           (None, 8)               136
dense_2 (Dense)           (None, 3)                27
=====================================================
Total params: 339

손 계산 : 176 + 136 + 27 = 339
실제 값 : 339

왜 이렇게 푸는가

층마다 계산식은 언제나 동일합니다.

파라미터 수 = (앞 층 노드 수 × 이 층 노드 수) + 이 층 노드 수

앞부분이 가중치, 뒤에 더하는 것이 노드마다 하나씩 있는 편향입니다.

  • 1층: (10 × 16) + 16 = 176
  • 2층: (16 × 8) + 8 = 136
  • 3층: (8 × 3) + 3 = 27
  • 합계: 339

여기서 "앞 층 노드 수"의 첫 값은 Input(shape=(10,))의 10입니다. Input은 계산을 하지 않으므로 summary() 표에 행으로 나타나지 않지만, 첫 Dense의 가중치 크기를 정하는 근거가 됩니다.

자주 틀리는 지점

  • 편향을 빼고 세는 것 — 10×16 + 16×8 + 8×3 = 312(3번)가 되어 딱 편향 개수(16+8+3=27)만큼 모자랍니다. 오답 3번이 이 함정입니다.
  • 출력층(Dense(3))을 파라미터 합계에서 빼먹는 것. 출력층도 가중치와 편향을 가진 학습 대상 층입니다.
  • Input을 빠뜨린 채 summary()를 부르면 Total params가 0으로 나오거나 "모델이 아직 build되지 않았다"는 메시지가 뜹니다. 입력 크기를 모르면 가중치 행렬 크기를 정할 수 없기 때문입니다.

Q3 해답

정답 코드

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense

X_tr = np.zeros((800, 15))

n_features = X_tr.shape[1]                       # (가)

model = Sequential()
model.add(Input(shape=(n_features,)))            # (나)
model.add(Dense(64, activation='relu'))
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))        # (다)

model.summary()
print("층 개수(Dense만):", len(model.layers))
print("출력 shape:", model.output_shape)
Layer (type)              Output Shape        Param #
=====================================================
dense (Dense)             (None, 64)            1,024
dense_1 (Dense)           (None, 32)            2,080
dense_2 (Dense)           (None, 1)                33
=====================================================
Total params: 3,137

층 개수(Dense만): 3
출력 shape: (None, 1)

Total params: (15 × 64 + 64) + (64 × 32 + 32) + (32 × 1 + 1) = 1024 + 2080 + 33 → 3,137

왜 이렇게 푸는가

  • (가) X_tr.shape[1] — shape는 (샘플 수, 특성 수)이므로 필요한 것은 인덱스 1입니다. 숫자 15를 직접 적지 않는 이유는 Q1 해설과 같습니다.
  • (나) Input(shape=(n_features,)) — Keras 2와 3 양쪽에서 동작하는 표준 표기입니다. 구버전 표기 Dense(64, activation='relu', input_shape=(n_features,))는 읽을 줄만 알면 되고, 새로 쓸 때는 Input을 씁니다.
  • (다) Dense(1, activation='sigmoid') — 타깃이 0 또는 1인 이진분류이므로 출력은 "1일 확률" 하나입니다. 노드 1개 + sigmoid가 짝입니다.

print(len(model.layers))가 3인 것도 확인 포인트입니다. Input은 입력 형태 선언일 뿐 연산 층이 아니어서 층 목록에 세지 않습니다.

자주 틀리는 지점

  • 은닉층 개수 세기 — "은닉층 2개 이상"에서 은닉층은 입력층과 출력층을 뺀 중간층입니다. Dense(64) 하나만 두고 출력층을 붙이면 은닉층은 1개뿐이라 조건 미달입니다.
  • X_tr.shape[0](=800)을 쓰는 실수. 인덱스 0은 샘플 수입니다.
  • 출력층에 activation='relu'를 그대로 두는 것. relu는 0 이상 아무 값이나 내므로 확률로 해석할 수 없습니다.

Q4 해답

정답 코드

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense, Dropout, BatchNormalization

X_tr = np.zeros((10, 20))
y_train = np.array([0, 2, 1, 3, 2, 0, 1, 3, 3, 1])

n_features = X_tr.shape[1]
n_class = len(np.unique(y_train))                 # (가)  -> 4

model = Sequential()
model.add(Input(shape=(n_features,)))
model.add(Dense(64, activation='relu'))
model.add(BatchNormalization())                   # (나)
model.add(Dropout(0.3))                           # (다)
model.add(Dense(32, activation='relu'))
model.add(Dropout(0.3))
model.add(Dense(n_class, activation='softmax'))   # (라)

model.summary()
print("클래스 개수:", n_class)
print("출력 shape:", model.output_shape)
Layer (type)                     Output Shape       Param #
===========================================================
dense (Dense)                    (None, 64)            1,344
batch_normalization              (None, 64)              256
dropout (Dropout)                (None, 64)                0
dense_1 (Dense)                  (None, 32)            2,080
dropout_1 (Dropout)              (None, 32)                0
dense_2 (Dense)                  (None, 4)               132
===========================================================
Total params: 3,812
Trainable params: 3,684
Non-trainable params: 128

클래스 개수: 4
출력 shape: (None, 4)

BatchNormalization의 Param #: 64 × 4 = 256이 찍히고, 그중 절반인 128이 Non-trainable입니다. 학습으로 조정하는 값 두 종류와, 학습 중 관찰한 통계를 저장해 두는 값 두 종류가 함께 들어 있기 때문입니다. 오류가 아니므로 그대로 두면 됩니다.

왜 이렇게 푸는가

  • (가) len(np.unique(y_train)) — 클래스 개수는 눈으로 세지 말고 데이터에서 뽑습니다. 라벨에 0~3이 들어 있으므로 4가 나옵니다. 시험에서는 클래스가 5개, 7개인 데이터도 나오며, 손으로 센 숫자는 전처리 결과가 바뀌면 그대로 틀립니다.
  • (나)(다) BatchNormalization() → Dropout(0.3) 순서 — 둘 다 은닉층 Dense 뒤에 붙습니다. BatchNormalization이 층을 지나는 값의 분포를 먼저 정돈하고, 그다음 Dropout이 노드 일부를 끕니다. 순서를 바꿔도 실행은 되지만 교재 표준 배치는 Dense → BatchNormalization → Dropout입니다.
  • (라) Dense(n_class, activation='softmax') — 다중분류 출력층은 클래스 개수만큼 노드를 두고 softmax로 합이 1인 확률 분포를 만듭니다. 출력 shape가 (None, 4)인지 summary()로 반드시 확인하십시오.

Dropout(0.3)의 0.3은 끄는 비율이지 남기는 비율이 아닙니다. Dropout 층의 Param #가 0인 것도 정상입니다. 노드를 끄기만 할 뿐 학습하는 값이 없습니다.

자주 틀리는 지점

  • 출력층 units와 클래스 수 불일치 — Dense(3, 'softmax')처럼 클래스 수보다 적게 두면 구성 단계에서는 아무 말이 없다가 학습 단계에서 라벨과 출력 개수가 맞지 않아 오류가 납니다. len(np.unique(y))로 뽑으면 이 사고가 사라집니다.
  • BatchNormalization을 괄호 없이 model.add(BatchNormalization)으로 쓰는 것. 층은 인스턴스를 만들어 넣어야 하므로 ()가 필요합니다.
  • BatchNormalization의 Non-trainable params를 보고 "모델이 잘못 만들어졌다"고 판단해 층을 지우는 것. 정상 동작입니다.

Q5 해답

잘못된 줄: model.add(Input(shape=X_train.shape))
X_train.shape는 (1000, 8)이므로 샘플 수 1000이 입력 shape에 그대로 들어갔습니다.

정답 코드

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense

X_train = np.zeros((1000, 8))

# --- 잘못된 코드의 결과 확인 ---
bad = Sequential()
bad.add(Input(shape=X_train.shape))              # (1000, 8) 이 통째로 들어감
bad.add(Dense(32, activation='relu'))
bad.add(Dense(1, activation='sigmoid'))
print("[잘못] 출력 shape:", bad.output_shape)

# --- 수정 ---
fixed = Sequential()
fixed.add(Input(shape=(X_train.shape[1],)))      # 특성 수만 튜플로
fixed.add(Dense(32, activation='relu'))
fixed.add(Dense(1, activation='sigmoid'))
print("[수정] 출력 shape:", fixed.output_shape)
fixed.summary()
[잘못] 출력 shape: (None, 1000, 1)
[수정] 출력 shape: (None, 1)

왜 이렇게 푸는가

이 오류가 위험한 이유는 실행이 성공한다는 데 있습니다. 오류 메시지가 없으니 그대로 다음 단계로 넘어가기 쉽습니다.

잘못된 코드의 출력 shape는 (None, 1000, 1)입니다. "샘플 하나가 1000줄짜리 표"라고 선언한 셈이므로, 모델은 샘플마다 예측을 1,000개씩 만들어 냅니다. 정답 y_train은 샘플당 값 하나이므로 학습 단계에서 shape 불일치 오류로 이어집니다.

수정은 X_train.shape[1]로 특성 수만 꺼내 원소 하나짜리 튜플로 넘기는 것입니다. 수정 후 출력 shape가 (None, 1), 즉 "샘플당 확률 1개"로 바뀝니다.

자주 틀리는 지점

  • shape=X_train.shape처럼 shape를 통째로 넘기는 것. 편해 보이지만 항상 샘플 수가 딸려 들어갑니다.
  • 오류가 학습 단계에서 터지기 때문에 원인을 fit() 쪽에서 찾는 것. 구성 직후 model.summary()를 한 번 호출해 출력 shape가 (None, 1)인지 눈으로 확인하면 이 시간 낭비를 막을 수 있습니다.
  • 고칠 때 Input(shape=(8))으로 쉼표를 빠뜨리는 것. ValueError: Cannot convert '8' to a shape.가 납니다.

Q6 해답

잘못된 곳 2군데

  1. model.add(Dense(2, activation='softmax')) — y_train의 클래스는 4개인데 출력층 노드가 2개입니다. 출력 shape가 (None, 2)가 되어 클래스 4개를 표현할 수 없습니다.
  2. model.add(Dropout(0.3))이 출력층 뒤에 붙었습니다. 마지막 Dense는 언제나 모델의 맨 아래에 있어야 합니다.

정답 코드

import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense, Dropout

y_train = np.array([0, 1, 2, 3, 1, 0, 3, 2])
n_class = len(np.unique(y_train))                  # 4

model = Sequential()
model.add(Input(shape=(20,)))
model.add(Dense(64, activation='relu'))
model.add(Dropout(0.3))
model.add(Dense(32, activation='relu'))
model.add(Dropout(0.3))                            # 은닉층 뒤로 이동
model.add(Dense(n_class, activation='softmax'))    # units를 클래스 수에 맞춤

model.summary()
print("출력 shape:", model.output_shape)
print("마지막 층 :", type(model.layers[-1]).__name__)
Layer (type)              Output Shape        Param #
=====================================================
dense (Dense)             (None, 64)            1,344
dropout (Dropout)         (None, 64)                0
dense_1 (Dense)           (None, 32)            2,080
dropout_1 (Dropout)       (None, 32)                0
dense_2 (Dense)           (None, 4)               132
=====================================================
Total params: 3,556

출력 shape: (None, 4)
마지막 층 : Dense

수정 전 코드를 그대로 실행하면 출력 shape: (None, 2), 마지막 층: Dropout이 나옵니다. 이 두 줄만 확인해도 오류 두 개를 동시에 잡아낼 수 있습니다.

왜 이렇게 푸는가

  • 출력층 units는 클래스 개수와 정확히 같아야 합니다. 다중분류 출력은 "각 클래스일 확률"의 목록이므로, 노드가 2개면 클래스 4개 중 2개분의 확률만 만들어집니다. 구성 단계에서는 오류가 나지 않고 학습을 시작해야 문제가 드러나므로, n_class = len(np.unique(y_train))로 뽑아 쓰는 습관이 유일한 예방책입니다.
  • Dropout은 은닉층 Dense 뒤에만 넣습니다. 출력층 뒤에 두면 최종 확률값 자체가 무작위로 0이 되어 예측 결과가 망가집니다. Dropout은 "학습이 잘 되도록 돕는 보조 층"이지 예측값을 만드는 층이 아니라는 점을 기억하십시오.

두 오류의 공통점은 summary() 한 번이면 잡힌다는 것입니다. 마지막 행이 Dense인지, Output Shape 뒷자리가 클래스 수와 같은지 — 구성 직후 이 두 가지만 확인하는 습관을 들이십시오.

자주 틀리는 지점

  • 클래스 개수를 눈으로 세다 틀리는 것. 라벨이 1부터 시작하거나(1~4) 중간 번호가 비어 있는 데이터에서 특히 자주 발생합니다.
  • 출력층 뒤 Dropout을 "마지막에 정규화를 한 번 더 하는 것"으로 잘못 이해하는 것.
  • 오류를 하나만 찾고 넘어가는 것. 실기에서는 한 셀 안에 실수가 겹치는 경우가 흔하므로, 구성 코드는 위에서 아래로 층 순서를 소리 내어 읽듯 검토하십시오.