콘텐츠로 이동

7차시: 활성화 함수·손실 함수·옵티마이저 - 학습의 3대 엔진 튜닝

⏰ 80분 · 활성화 함수 · 손실 함수 · 옵티마이저 · 하이퍼파라미터 · 난이도 ●●●○○

학습목표: 활성화 함수의 특성을 비교하여 상황에 맞게 선택하고, 손실 함수와 옵티마이저의 조합을 실험으로 튜닝할 수 있다.

오늘의 질문: “같은 모델인데 왜 어떤 설정은 5분 만에 학습되고, 어떤 설정은 한 시간이 지나도 제자리일까요?”


뉴런의 “켜짐/꺼짐”을 결정하는 스위치. Sigmoid, ReLU, Softmax를 구분합니다.

모델이 얼마나 틀렸는지 측정하는 자. MSE와 Cross-Entropy를 비교합니다.

SGD, Momentum, Adam을 같은 문제에 넣고 학습 곡선을 비교합니다.

하이퍼파라미터를 바꿔가며 최고 성능 조합을 찾습니다.


1단계: 도입 — 왜 엔진 튜닝이 필요한가 (10분)

섹션 제목: “1단계: 도입 — 왜 엔진 튜닝이 필요한가 (10분)”

동일한 모델을 활성화 함수만 바꿔서 실행한 결과를 비교합니다. 같은 데이터, 같은 구조인데도 정확도가 50%와 95%로 갈리는 장면을 먼저 목격합니다.

Sigmoid, Tanh, ReLU, Softmax의 그래프와 수식을 비교합니다. 기울기 소실 문제를 NumPy로 시각화합니다.

3단계: 손실 함수 — MSE vs Cross-Entropy (15분)

섹션 제목: “3단계: 손실 함수 — MSE vs Cross-Entropy (15분)”

분류 문제에 MSE를 쓰면 왜 느린지 수식과 그래프로 확인합니다. Keras로 동일 모델의 손실 함수만 바꿔 수렴 속도를 비교합니다.

SGD, Momentum, Adam을 동일 문제에 적용하여 학습 곡선을 겹쳐 그립니다. 학습률을 바꿔가며 발산·수렴 경계를 찾습니다.

5단계: 최강 조합 챌린지 + 정리 (10분)

섹션 제목: “5단계: 최강 조합 챌린지 + 정리 (10분)”

MNIST 소형 모델에서 활성화·손실·옵티마이저를 자유 조합해 가장 빨리 98%에 도달하는 레시피를 탐색합니다. 형성 평가로 마무리합니다.


실습 환경: Python 3.10+, pip install tensorflow numpy matplotlib

본격 개념으로 들어가기 전에, 왜 이 세 가지를 배워야 하는지 숫자로 먼저 느껴보겠습니다. 아래 코드는 MNIST 숫자 분류 모델을 두 가지 설정으로 학습시킵니다.

import tensorflow as tf
from tensorflow.keras import layers, models
import numpy as np
# 데이터 준비 (60,000장 손글씨 숫자)
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
x_train = x_train.reshape(-1, 784).astype("float32") / 255.0
x_test = x_test.reshape(-1, 784).astype("float32") / 255.0
def build_model(activation):
# 같은 구조, 활성화 함수만 바꿈
model = models.Sequential([
layers.Dense(128, activation=activation, input_shape=(784,)),
layers.Dense(128, activation=activation),
layers.Dense(10, activation="softmax"),
])
return model
# 설정 A: Sigmoid + SGD (학습률 0.01)
model_a = build_model("sigmoid")
model_a.compile(optimizer=tf.keras.optimizers.SGD(0.01),
loss="sparse_categorical_crossentropy", metrics=["accuracy"])
hist_a = model_a.fit(x_train, y_train, epochs=5, batch_size=128, verbose=0)
# 설정 B: ReLU + Adam (학습률 0.001)
model_b = build_model("relu")
model_b.compile(optimizer=tf.keras.optimizers.Adam(0.001),
loss="sparse_categorical_crossentropy", metrics=["accuracy"])
hist_b = model_b.fit(x_train, y_train, epochs=5, batch_size=128, verbose=0)
print(f"[A] Sigmoid+SGD 최종 정확도: {hist_a.history['accuracy'][-1]:.3f}")
print(f"[B] ReLU+Adam 최종 정확도: {hist_b.history['accuracy'][-1]:.3f}")

실행 결과 (수치는 실행마다 약간 다를 수 있습니다):

[A] Sigmoid+SGD 최종 정확도: 0.412
[B] ReLU+Adam 최종 정확도: 0.978

구조가 완전히 동일한 모델입니다. 바뀐 것은 활성화 함수옵티마이저뿐입니다. 그런데 5 에폭 후 정확도 차이는 41% vs 98%. 이 차이를 만드는 세 가지 부품을 이번 차시에 하나씩 뜯어보겠습니다.


개념 1: 활성화 함수 — 뉴런의 “발화 스위치”

섹션 제목: “개념 1: 활성화 함수 — 뉴런의 “발화 스위치””

뇌의 뉴런은 입력 신호가 일정 세기를 넘으면 발화하고, 아니면 조용합니다. 인공신경망의 활성화 함수도 같은 역할을 합니다. 뉴런에 들어온 값 $z = \mathbf{w}^\top \mathbf{x} + b$ 를 얼마나 통과시킬지 결정합니다.

활성화 함수가 없다면? 신경망은 아무리 층을 쌓아도 결국 하나의 직선에 불과합니다. 선형의 합성은 선형이기 때문입니다. 비선형 활성화 함수가 바로 딥러닝에 “깊이의 힘”을 주는 장본인입니다.

함수수식출력 범위용도특징
Sigmoid$\sigma(z)=\dfrac{1}{1+e^{-z}}$(0, 1)이진 분류 출력층기울기 소실 심함
Tanh$\tanh(z)=\dfrac{e^z-e^{-z}}{e^z+e^{-z}}$(-1, 1)RNN 은닉층0 중심, 여전히 소실
ReLU$\max(0, z)$[0, ∞)은닉층 기본값빠르고 간단, 죽은 뉴런 문제
Softmax$\dfrac{e^{z_i}}{\sum_j e^{z_j}}$(0, 1), 합=1다중 분류 출력층확률 분포 생성
flowchart LR
    A[입력 z] --> B{층 종류}
    B -->|은닉층| C[ReLU 우선]
    B -->|이진 분류 출력| D[Sigmoid]
    B -->|다중 분류 출력| E[Softmax]
    B -->|회귀 출력| F[활성화 없음]

기울기 소실 문제 직접 확인하기

섹션 제목: “기울기 소실 문제 직접 확인하기”

Sigmoid가 왜 위험한지 직접 그려보겠습니다.

import numpy as np
import matplotlib.pyplot as plt
z = np.linspace(-10, 10, 200)
sigmoid = 1 / (1 + np.exp(-z))
# 미분: sigmoid * (1 - sigmoid)
sigmoid_grad = sigmoid * (1 - sigmoid)
relu = np.maximum(0, z)
relu_grad = (z > 0).astype(float) # <- 양수면 1, 음수면 0
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].plot(z, sigmoid, label="sigmoid")
axes[0].plot(z, relu/10, label="ReLU/10 (스케일조정)")
axes[0].set_title("활성화 함수 값"); axes[0].legend(); axes[0].grid(True)
axes[1].plot(z, sigmoid_grad, label="sigmoid'")
axes[1].plot(z, relu_grad, label="ReLU'")
axes[1].set_title("미분값 (기울기)"); axes[1].legend(); axes[1].grid(True)
plt.tight_layout(); plt.show()
print(f"Sigmoid 최대 기울기: {sigmoid_grad.max():.3f}")
print(f"ReLU 기울기: 0 또는 1 (양수에서 항상 1)")
Sigmoid 최대 기울기: 0.250
ReLU 기울기: 0 또는 1 (양수에서 항상 1)

Sigmoid의 최대 기울기는 0.25입니다. 층을 10개 쌓으면 역전파 시 기울기가 최악의 경우 $0.25^{10} \approx 10^{-6}$ 까지 작아집니다. 기울기가 사라지면 가중치 업데이트가 멈춥니다. 이것이 바로 기울기 소실(vanishing gradient) 입니다.

위 코드의 7번째 줄 sigmoid_grad = sigmoid * (1 - sigmoid)가 바로 기울기 소실의 원인입니다. 최댓값이 0.25로 제한되기 때문입니다. 반면 11번째 줄 relu_grad = (z > 0).astype(float)는 양수 구간에서 항상 1이라 기울기가 그대로 전달됩니다.

💡 실무 팁: 은닉층은 고민 말고 ReLU부터 시작하세요. 출력층만 문제 성격에 맞게 선택합니다 — 이진 분류는 Sigmoid, 다중 분류는 Softmax, 회귀는 활성화 없음.


개념 2: 손실 함수 — “얼마나 틀렸나”의 자

섹션 제목: “개념 2: 손실 함수 — “얼마나 틀렸나”의 자”

손실 함수는 모델의 예측과 정답 사이 거리를 숫자 하나로 요약합니다. 이 숫자가 작아지도록 가중치를 조정하는 것이 학습입니다. 즉, 손실 함수의 모양이 학습의 방향을 결정합니다.

자주 쓰는 두 가지를 비교해보겠습니다.

MSE (Mean Squared Error) — 회귀의 표준

섹션 제목: “MSE (Mean Squared Error) — 회귀의 표준”

$$ \text{MSE} = \dfrac{1}{n}\sum_{i=1}^{n}(y_i - \hat{y}_i)^2 $$

  • $y_i$: 실제 값, $\hat{y}_i$: 예측 값
  • 용도: 집값 예측, 온도 예측처럼 실수를 예측하는 회귀 문제

이진 분류의 경우:

$$ \text{BCE} = -\dfrac{1}{n}\sum_{i=1}^{n}\Big[y_i \log \hat{y}_i + (1-y_i)\log(1-\hat{y}_i)\Big] $$

  • $\hat{y}_i$: 모델이 예측한 “정답일 확률” (0-1 사이)
  • 용도: 고양이/개 분류, 숫자 인식처럼 분류 문제

분류 문제에 MSE를 쓰면 어떻게 될까요?

섹션 제목: “분류 문제에 MSE를 쓰면 어떻게 될까요?”

실험으로 확인하겠습니다. 이진 분류 모델을 MSE로도 학습시키고 BCE로도 학습시킵니다.

import tensorflow as tf
from tensorflow.keras import layers, models
import numpy as np
# 간단한 이진 분류 데이터 (두 가우시안 분포)
np.random.seed(42)
X = np.vstack([np.random.randn(500, 2) - 1.5,
np.random.randn(500, 2) + 1.5])
y = np.array([0]*500 + [1]*500)
def build(loss_name):
m = models.Sequential([
layers.Dense(16, activation="relu", input_shape=(2,)),
layers.Dense(1, activation="sigmoid"), # <- 확률 출력
])
m.compile(optimizer=tf.keras.optimizers.SGD(0.1),
loss=loss_name, metrics=["accuracy"])
return m
model_mse = build("mse")
hist_mse = model_mse.fit(X, y, epochs=30, batch_size=32, verbose=0)
model_bce = build("binary_crossentropy")
hist_bce = model_bce.fit(X, y, epochs=30, batch_size=32, verbose=0)
print(f"MSE로 학습 5 에폭 후 정확도: {hist_mse.history['accuracy'][4]:.3f}")
print(f"BCE로 학습 5 에폭 후 정확도: {hist_bce.history['accuracy'][4]:.3f}")
print(f"MSE 30 에폭 최종: {hist_mse.history['accuracy'][-1]:.3f}")
print(f"BCE 30 에폭 최종: {hist_bce.history['accuracy'][-1]:.3f}")
MSE로 학습 5 에폭 후 정확도: 0.612
BCE로 학습 5 에폭 후 정확도: 0.958
MSE 30 에폭 최종: 0.923
BCE 30 에폭 최종: 0.992

같은 모델인데 BCE가 훨씬 빠르게 수렴합니다. 왜일까요?

Sigmoid 출력과 MSE를 조합하면 손실 함수의 기울기에 $\sigma’(z) = \sigma(z)(1-\sigma(z))$ 가 곱해집니다. 예측이 크게 틀렸을 때(예: 정답은 1인데 0.01로 예측) $\sigma’ \approx 0.01 \times 0.99 \approx 0.01$ 이라 기울기가 거의 사라집니다. 학습이 느려지는 이유입니다.

반면 BCE는 미분 시 $\sigma’$ 항이 수식적으로 소거됩니다. 기울기가 단순히 $(\hat{y} - y)$ 로 깔끔하게 떨어져, 틀릴수록 큰 기울기가 나옵니다.

문제 유형출력층 활성화권장 손실 함수
회귀 (실수 예측)없음MSE
이진 분류SigmoidBinary Cross-Entropy
다중 분류 (one-hot)SoftmaxCategorical Cross-Entropy
다중 분류 (정수 라벨)SoftmaxSparse Categorical Cross-Entropy

개념 3: 옵티마이저 — 경사하강법의 진화

섹션 제목: “개념 3: 옵티마이저 — 경사하강법의 진화”

손실 함수가 “골짜기 지형”이라면, 옵티마이저는 골짜기를 내려가는 방법입니다. 같은 산이라도 걸어서 내려가느냐, 스키를 타느냐에 따라 속도와 안정성이 달라집니다.

$$ \mathbf{w} \leftarrow \mathbf{w} - \eta abla L(\mathbf{w}) $$

  • $\eta$: 학습률(learning rate) — 한 걸음의 크기
  • 단순하고 이해하기 쉬우나, 지그재그로 흔들리며 느리게 수렴합니다.

지난 걸음의 방향을 관성으로 이어받습니다. 언덕에서 공이 굴러가는 느낌입니다.

$$ \begin{aligned} \mathbf{v} &\leftarrow \beta \mathbf{v} + abla L(\mathbf{w}) \ \mathbf{w} &\leftarrow \mathbf{w} - \eta \mathbf{v} \end{aligned} $$

  • $\beta$: 관성 계수 (보통 0.9). 과거 방향의 90%를 유지합니다.
  • 지그재그를 줄이고 골짜기 바닥을 따라 쭉쭉 나아갑니다.

각 가중치마다 학습률을 다르게 자동 조정합니다. 자주 업데이트되는 가중치는 작게, 덜 업데이트되는 가중치는 크게 걸음을 내딛습니다.

$$ \begin{aligned} \mathbf{m} &\leftarrow \beta_1 \mathbf{m} + (1-\beta_1) abla L \ \mathbf{v} &\leftarrow \beta_2 \mathbf{v} + (1-\beta_2)( abla L)^2 \ \mathbf{w} &\leftarrow \mathbf{w} - \eta \dfrac{\hat{\mathbf{m}}}{\sqrt{\hat{\mathbf{v}}}+\epsilon} \end{aligned} $$

기본 하이퍼파라미터($\beta_1=0.9, \beta_2=0.999, \eta=0.001$)가 거의 모든 문제에서 잘 작동해서 실무의 기본 선택지입니다.

옵티마이저수렴 속도하이퍼파라미터 민감도추천 상황
SGD느림높음 (lr 튜닝 필수)논문 재현, 이론 학습
SGD + Momentum중간중간대형 이미지 모델 (ResNet 등)
Adam빠름낮음거의 모든 실무 기본값

개념 4: 옵티마이저 경주 — 눈으로 보는 수렴 차이

섹션 제목: “개념 4: 옵티마이저 경주 — 눈으로 보는 수렴 차이”

세 옵티마이저를 같은 문제에 적용해 학습 곡선을 겹쳐 그려보겠습니다.```python import tensorflow as tf from tensorflow.keras import layers, models import matplotlib.pyplot as plt

Fashion MNIST로 경주 (MNIST보다 살짝 어려워서 차이가 잘 보임)

섹션 제목: “Fashion MNIST로 경주 (MNIST보다 살짝 어려워서 차이가 잘 보임)”

(x_tr, y_tr), _ = tf.keras.datasets.fashion_mnist.load_data() x_tr = x_tr.reshape(-1, 784).astype(“float32”) / 255.0

def make_model(): # 같은 초기값에서 출발하도록 seed 고정 tf.random.set_seed(7) return models.Sequential([ layers.Dense(64, activation=“relu”, input_shape=(784,)), layers.Dense(64, activation=“relu”), layers.Dense(10, activation=“softmax”), ])

optimizers = { “SGD(lr=0.01)”: tf.keras.optimizers.SGD(0.01), “Momentum(lr=0.01)”: tf.keras.optimizers.SGD(0.01, momentum=0.9), # <- 관성 추가 “Adam(lr=0.001)”: tf.keras.optimizers.Adam(0.001), }

histories = {} for name, opt in optimizers.items(): model = make_model() model.compile(optimizer=opt, loss=“sparse_categorical_crossentropy”, metrics=[“accuracy”]) h = model.fit(x_tr, y_tr, epochs=10, batch_size=128, verbose=0) histories[name] = h.history

plt.figure(figsize=(9, 4)) for name, h in histories.items(): plt.plot(h[“loss”], label=name) plt.xlabel(“Epoch”); plt.ylabel(“Loss”); plt.legend(); plt.grid(True) plt.title(“옵티마이저별 학습 곡선”); plt.show()

for name, h in histories.items(): print(f”{name:25s} → 최종 loss: {h[‘loss’][-1]:.4f}, 정확도: {h[‘accuracy’][-1]:.3f}”)

실행 결과 예시:

SGD(lr=0.01) → 최종 loss: 0.5812, 정확도: 0.798 Momentum(lr=0.01) → 최종 loss: 0.3421, 정확도: 0.877 Adam(lr=0.001) → 최종 loss: 0.2903, 정확도: 0.893

같은 에폭, 같은 모델인데 <strong>Adam이 loss를 절반 가까이 낮춥니다</strong>. 위 코드의 18번째 줄 `tf.keras.optimizers.SGD(0.01, momentum=0.9)` 가 바로 <strong>Momentum</strong>입니다. `momentum` 인자 하나를 추가한 것만으로 정확도가 8%p 뛰었습니다.
#### ⚠️ 학습률을 너무 크게 주면? — 에러 경험
"학습률 크면 빨리 가겠지"라는 생각으로 아래처럼 설정하면 어떤 일이 생길까요?
```python
model = make_model()
model.compile(optimizer=tf.keras.optimizers.SGD(learning_rate=5.0), # <- 너무 큼
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
h = model.fit(x_tr, y_tr, epochs=5, batch_size=128, verbose=1)

결과:

Epoch 1/5 loss: 2.8341 - accuracy: 0.1023
Epoch 2/5 loss: nan - accuracy: 0.1000
Epoch 3/5 loss: nan - accuracy: 0.1000

loss가 NaN(Not a Number) 이 되어버립니다. 학습률이 너무 크면 최저점을 지나 반대편 언덕으로 튀어 올라가고, 그 다음엔 더 멀리 튀고, 결국 무한대로 발산합니다. 수정은 간단합니다 — 학습률을 0.01 정도로 낮추면 됩니다.

💡 경험칙: Adam은 0.001, SGD는 0.01부터 시작합니다. 발산하면 10배 줄이고, 너무 느리면 3배 키워보세요.


다음 조건에서 가장 적은 에폭으로 검증 정확도 97%를 달성하는 조합을 찾습니다.

  • 데이터: MNIST (손글씨 숫자)
  • 모델: 은닉층 2개짜리 Dense 네트워크
  • 바꿀 수 있는 것: 활성화 함수, 옵티마이저, 학습률, 배치 크기
  • 바꿀 수 없는 것: 층 수, 뉴런 수(128, 128), 출력층 Softmax, 손실 함수(sparse_categorical_crossentropy)
import tensorflow as tf
from tensorflow.keras import layers, models
(x_tr, y_tr), (x_te, y_te) = tf.keras.datasets.mnist.load_data()
x_tr = x_tr.reshape(-1, 784).astype("float32") / 255.0
x_te = x_te.reshape(-1, 784).astype("float32") / 255.0
def run(activation, optimizer, batch_size, epochs=10):
tf.random.set_seed(0)
model = models.Sequential([
layers.Dense(128, activation=activation, input_shape=(784,)),
layers.Dense(128, activation=activation),
layers.Dense(10, activation="softmax"),
])
model.compile(optimizer=optimizer,
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
h = model.fit(x_tr, y_tr,
validation_data=(x_te, y_te),
epochs=epochs, batch_size=batch_size, verbose=0)
# 검증 정확도 97% 최초 도달 에폭 찾기
for i, acc in enumerate(h.history["val_accuracy"]):
if acc >= 0.97:
return i + 1, acc
return None, h.history["val_accuracy"][-1]
# 팀별로 아래 조합을 바꿔가며 기록지 채우기
ep, acc = run("relu", tf.keras.optimizers.Adam(0.001), batch_size=128)
print(f"도달 에폭: {ep}, 최종 val_acc: {acc:.4f}")

예시 결과:

도달 에폭: 2, 최종 val_acc: 0.9758

팀별로 최소 4가지 조합을 시도하고 기록합니다.

시도활성화옵티마이저학습률배치97% 도달 에폭메모
1ReLUAdam0.001128베이스라인
2
3
4

💡 힌트: 배치 크기를 줄이면(32, 64) 에폭당 업데이트 횟수가 늘어 빨리 수렴하는 경우가 많습니다. 하지만 너무 작으면 불안정해집니다.


🤔 토론 활동 — “엔진 선택 위원회”

섹션 제목: “🤔 토론 활동 — “엔진 선택 위원회””

활동 유형: 모둠(4인) · 10분

여러분은 새 AI 프로젝트의 엔진 선택 위원회입니다. 아래 세 가지 상황에 대해 “활성화-손실-옵티마이저” 조합을 결정하고 이유를 정리하세요.

  1. 상황 A: 아파트 가격을 예측하는 모델. 출력은 실수값(단위: 만원).
  2. 상황 B: 환자의 엑스레이 사진으로 폐렴/정상을 판별하는 모델.
  3. 상황 C: 꽃 사진을 장미·튤립·해바라기·백합·국화 5종류로 분류하는 모델.
참고 답안
상황출력 활성화손실 함수옵티마이저
A (회귀)없음MSEAdam(0.001)
B (이진 분류)SigmoidBinary Cross-EntropyAdam(0.001)
C (다중 분류)Softmax(5)Sparse Categorical Cross-EntropyAdam(0.001)

은닉층은 모두 ReLU로 시작하면 됩니다.


  • 이미지 분류 (ResNet, EfficientNet): 은닉층 ReLU, 출력층 Softmax, 손실 Categorical Cross-Entropy, 옵티마이저는 SGD+Momentum에 학습률 스케줄링을 씁니다. 대형 모델에서는 Adam보다 일반화 성능이 좋을 때가 많습니다.
  • 자연어 처리 (BERT, GPT): 은닉층은 GELU(ReLU의 부드러운 친척), 옵티마이저는 AdamW(Adam + 가중치 감쇠)가 거의 표준입니다.
  • 추천 시스템: 출력층은 Sigmoid(클릭 확률), 손실은 Binary Cross-Entropy, 옵티마이저는 Adam을 사용합니다.
  • Kaggle 대회 스타터 팩: 막막하면 일단 activation="relu", optimizer=Adam(1e-3), loss=적절한 cross-entropy로 베이스라인을 만들고, 성능이 붙으면 그때 튜닝합니다.

다음 문제에 맞는 출력층 활성화 함수와 손실 함수를 채우세요.

  • 영화 리뷰의 별점(1.0-5.0)을 예측 → 활성화: ( ), 손실: ( )
  • 이메일 스팸 여부 판별 → 활성화: ( ), 손실: ( )
  • 10종 동물 이미지 분류 → 활성화: ( ), 손실: ( )
정답
  • 별점 회귀: 없음, MSE
  • 스팸 이진 분류: Sigmoid, Binary Cross-Entropy
  • 10종 다중 분류: Softmax, Sparse Categorical Cross-Entropy (정수 라벨일 경우)

Adam 옵티마이저의 학습률을 [0.1, 0.01, 0.001, 0.0001] 로 바꿔가며 MNIST에서 3 에폭씩 돌려, 최종 val_accuracy가 가장 높은 학습률을 찾는 코드를 작성하세요.

힌트

for 루프 안에서 tf.keras.optimizers.Adam(lr) 을 매번 새로 만들고, 결과를 딕셔너리에 저장하세요. 모델도 매번 새로 만들어야 공정한 비교가 됩니다.

정답 코드
import tensorflow as tf
from tensorflow.keras import layers, models
(x_tr, y_tr), (x_te, y_te) = tf.keras.datasets.mnist.load_data()
x_tr = x_tr.reshape(-1, 784).astype("float32") / 255.0
x_te = x_te.reshape(-1, 784).astype("float32") / 255.0
results = {}
for lr in [0.1, 0.01, 0.001, 0.0001]:
tf.random.set_seed(0)
m = models.Sequential([
layers.Dense(128, activation="relu", input_shape=(784,)),
layers.Dense(10, activation="softmax"),
])
m.compile(optimizer=tf.keras.optimizers.Adam(lr),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
h = m.fit(x_tr, y_tr, validation_data=(x_te, y_te),
epochs=3, batch_size=128, verbose=0)
results[lr] = h.history["val_accuracy"][-1]
print(f"lr={lr}: val_acc={results[lr]:.4f}")
best_lr = max(results, key=results.get)
print(f"최적 학습률: {best_lr}")

보통 0.001이 가장 좋고, 0.1은 발산에 가까운 결과가 나옵니다.

ReLU의 단점 중 하나는 죽은 뉴런(dead neuron) 입니다. 입력이 항상 음수가 되어버려 출력이 계속 0이고 기울기도 0이라 영원히 학습되지 않는 뉴런입니다. 학습이 끝난 모델의 첫 번째 은닉층에서, 훈련 데이터 전체에 대해 한 번도 0보다 큰 출력을 내지 않은 뉴런의 개수를 세는 코드를 작성하세요.

힌트

tf.keras.Model(inputs=model.input, outputs=model.layers[0].output) 으로 중간층 출력을 얻을 수 있습니다. 이 출력에 대해 각 뉴런별로 최댓값을 구하고, 그 최댓값이 0 이하인 뉴런을 세면 됩니다.

정답 코드
import numpy as np
import tensorflow as tf
from tensorflow.keras import layers, models
(x_tr, y_tr), _ = tf.keras.datasets.mnist.load_data()
x_tr = x_tr.reshape(-1, 784).astype("float32") / 255.0
model = models.Sequential([
layers.Dense(128, activation="relu", input_shape=(784,)),
layers.Dense(10, activation="softmax"),
])
model.compile(optimizer=tf.keras.optimizers.SGD(10.0), # 일부러 큰 lr로 죽은 뉴런 유도
loss="sparse_categorical_crossentropy", metrics=["accuracy"])
model.fit(x_tr, y_tr, epochs=3, batch_size=128, verbose=0)
# 첫 은닉층 출력 추출
extractor = tf.keras.Model(inputs=model.input, outputs=model.layers[0].output)
activations = extractor.predict(x_tr[:5000], verbose=0) # (5000, 128)
max_per_neuron = activations.max(axis=0) # 각 뉴런의 최대 출력
dead = np.sum(max_per_neuron <= 0)
print(f"죽은 뉴런 수: {dead} / 128")

학습률을 일부러 크게 해서 학습시키면 상당수 뉴런이 죽은 것을 확인할 수 있습니다. 이를 방지하려면 학습률을 적절히 낮추거나 Leaky ReLU 같은 변종을 사용합니다.


다중 분류(10개 클래스) 문제에서 출력층에 가장 적합한 활성화 함수는?

Sigmoid 은닉층을 깊게 쌓을 때 발생하는 대표적 문제는?

SGD와 Adam의 차이로 가장 정확한 설명은?

이진 분류 모델의 출력층에 Sigmoid를 쓸 때, 학습이 가장 빠른 손실 함수는?

서술형 1. 친구가 MNIST 분류 모델을 만들었는데, 학습 중 loss가 NaN이 되어 멈췄다고 합니다. 원인으로 무엇을 의심해볼 수 있고, 어떻게 수정하도록 조언하시겠습니까? 세 가지 이상의 가능성을 들어 설명하세요.

예시 답안
  1. 학습률이 너무 크다 — 가장 흔한 원인입니다. SGD에서 lr=1.0 이상, Adam에서 lr=0.1 이상이면 가중치가 발산하여 NaN이 됩니다. 학습률을 10배씩 줄여가며(예: 0.001 → 0.0001) 재시도합니다.
  2. 입력 데이터를 정규화하지 않았다 — 픽셀값 0-255를 그대로 넣으면 활성화값이 폭발합니다. x / 255.0 으로 0-1 범위로 스케일링해야 합니다.
  3. 손실 함수와 출력층 활성화 불일치 — 예컨대 출력층에 Softmax를 쓰면서 from_logits=True 옵션으로 손실을 계산하면 log(0)이 발생하여 NaN이 될 수 있습니다. 출력층 활성화와 손실 함수 설정을 재확인해야 합니다.

추가로 배치 정규화 누락, 초기값이 과도하게 큰 경우(표준편차 큰 초기화) 등도 원인이 될 수 있습니다.

자기점검 체크리스트

  • Sigmoid, Tanh, ReLU, Softmax를 용도별로 구분해서 말할 수 있다
  • 분류 문제에 MSE 대신 Cross-Entropy를 쓰는 이유를 설명할 수 있다
  • SGD, Momentum, Adam의 차이를 실험 결과와 함께 설명할 수 있다
  • 학습률이 너무 크거나 작을 때 생기는 증상을 구분할 수 있다
  • 오늘의 질문(“같은 모델, 다른 운명”)에 나만의 답을 말할 수 있다


오늘 수업에서 배운 것을 돌아보며 아래 질문에 스스로 답해봅니다.

  • 세 가지 부품(활성화·손실·옵티마이저) 중 가장 인상 깊었던 것은 무엇이며, 그 이유는?
  • 옵티마이저 경주 실험에서 예상과 달랐던 결과가 있었습니까? 있다면 무엇이었습니까?
  • 만약 이 중 하나의 설정을 잘못 골랐을 때 모델에 어떤 증상이 나타날지, 내 언어로 설명할 수 있습니까?
  • 오늘 배운 내용을 친구에게 1분 안에 설명한다면 어떤 비유를 쓰겠습니까?

다음 차시에서는 배치 정규화와 드롭아웃 을 다룹니다. 학습은 되는데 검증 정확도가 오르지 않는 과적합을 해결하는 기법들입니다. 오늘 만든 최강 조합에 정규화 기법을 얹어, 훈련과 검증 성능의 간격을 줄이는 실험을 이어갑니다.

생각해볼 질문: “같은 반 친구와 같은 책으로 공부했는데, 왜 어떤 친구는 시험 문제만 외우고 실전에서는 틀릴까요? 모델도 똑같은 일이 일어납니다.”