7차시: 활성화 함수·손실 함수·옵티마이저 - 학습의 3대 엔진 튜닝
🎯 이 차시의 핵심 주제
섹션 제목: “🎯 이 차시의 핵심 주제”🧠 활성화 함수
섹션 제목: “🧠 활성화 함수”뉴런의 “켜짐/꺼짐”을 결정하는 스위치. Sigmoid, ReLU, Softmax를 구분합니다.
📉 손실 함수
섹션 제목: “📉 손실 함수”모델이 얼마나 틀렸는지 측정하는 자. MSE와 Cross-Entropy를 비교합니다.
🏎️ 옵티마이저 경주
섹션 제목: “🏎️ 옵티마이저 경주”SGD, Momentum, Adam을 같은 문제에 넣고 학습 곡선을 비교합니다.
🏆 최강 조합 챌린지
섹션 제목: “🏆 최강 조합 챌린지”하이퍼파라미터를 바꿔가며 최고 성능 조합을 찾습니다.
⏱️ 수업 흐름
섹션 제목: “⏱️ 수업 흐름”1단계: 도입 — 왜 엔진 튜닝이 필요한가 (10분)
섹션 제목: “1단계: 도입 — 왜 엔진 튜닝이 필요한가 (10분)”동일한 모델을 활성화 함수만 바꿔서 실행한 결과를 비교합니다. 같은 데이터, 같은 구조인데도 정확도가 50%와 95%로 갈리는 장면을 먼저 목격합니다.
2단계: 활성화 함수 4형제 (20분)
섹션 제목: “2단계: 활성화 함수 4형제 (20분)”Sigmoid, Tanh, ReLU, Softmax의 그래프와 수식을 비교합니다. 기울기 소실 문제를 NumPy로 시각화합니다.
3단계: 손실 함수 — MSE vs Cross-Entropy (15분)
섹션 제목: “3단계: 손실 함수 — MSE vs Cross-Entropy (15분)”분류 문제에 MSE를 쓰면 왜 느린지 수식과 그래프로 확인합니다. Keras로 동일 모델의 손실 함수만 바꿔 수렴 속도를 비교합니다.
4단계: 옵티마이저 경주 (25분)
섹션 제목: “4단계: 옵티마이저 경주 (25분)”SGD, Momentum, Adam을 동일 문제에 적용하여 학습 곡선을 겹쳐 그립니다. 학습률을 바꿔가며 발산·수렴 경계를 찾습니다.
5단계: 최강 조합 챌린지 + 정리 (10분)
섹션 제목: “5단계: 최강 조합 챌린지 + 정리 (10분)”MNIST 소형 모델에서 활성화·손실·옵티마이저를 자유 조합해 가장 빨리 98%에 도달하는 레시피를 탐색합니다. 형성 평가로 마무리합니다.
🔥 도입: 같은 모델, 다른 운명
섹션 제목: “🔥 도입: 같은 모델, 다른 운명”실습 환경: Python 3.10+, pip install tensorflow numpy matplotlib
본격 개념으로 들어가기 전에, 왜 이 세 가지를 배워야 하는지 숫자로 먼저 느껴보겠습니다. 아래 코드는 MNIST 숫자 분류 모델을 두 가지 설정으로 학습시킵니다.
import tensorflow as tffrom tensorflow.keras import layers, modelsimport 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.0x_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 npimport 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.250ReLU 기울기: 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$: 예측 값
- 용도: 집값 예측, 온도 예측처럼 실수를 예측하는 회귀 문제
Cross-Entropy — 분류의 표준
섹션 제목: “Cross-Entropy — 분류의 표준”이진 분류의 경우:
$$ \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 tffrom tensorflow.keras import layers, modelsimport 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.612BCE로 학습 5 에폭 후 정확도: 0.958MSE 30 에폭 최종: 0.923BCE 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 |
| 이진 분류 | Sigmoid | Binary Cross-Entropy |
| 다중 분류 (one-hot) | Softmax | Categorical Cross-Entropy |
| 다중 분류 (정수 라벨) | Softmax | Sparse Categorical Cross-Entropy |
개념 3: 옵티마이저 — 경사하강법의 진화
섹션 제목: “개념 3: 옵티마이저 — 경사하강법의 진화”손실 함수가 “골짜기 지형”이라면, 옵티마이저는 골짜기를 내려가는 방법입니다. 같은 산이라도 걸어서 내려가느냐, 스키를 타느냐에 따라 속도와 안정성이 달라집니다.
SGD: 한 걸음씩 가장 기본
섹션 제목: “SGD: 한 걸음씩 가장 기본”$$ \mathbf{w} \leftarrow \mathbf{w} - \eta abla L(\mathbf{w}) $$
- $\eta$: 학습률(learning rate) — 한 걸음의 크기
- 단순하고 이해하기 쉬우나, 지그재그로 흔들리며 느리게 수렴합니다.
Momentum: 관성 추가
섹션 제목: “Momentum: 관성 추가”지난 걸음의 방향을 관성으로 이어받습니다. 언덕에서 공이 굴러가는 느낌입니다.
$$ \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%를 유지합니다.
- 지그재그를 줄이고 골짜기 바닥을 따라 쭉쭉 나아갑니다.
Adam: 적응적 학습률 + Momentum
섹션 제목: “Adam: 적응적 학습률 + Momentum”각 가중치마다 학습률을 다르게 자동 조정합니다. 자주 업데이트되는 가중치는 작게, 덜 업데이트되는 가중치는 크게 걸음을 내딛습니다.
$$ \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 뛰었습니다.
#### ⚠️ 학습률을 너무 크게 주면? — 에러 경험
"학습률 크면 빨리 가겠지"라는 생각으로 아래처럼 설정하면 어떤 일이 생길까요?
```pythonmodel = 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.1023Epoch 2/5 loss: nan - accuracy: 0.1000Epoch 3/5 loss: nan - accuracy: 0.1000loss가 NaN(Not a Number) 이 되어버립니다. 학습률이 너무 크면 최저점을 지나 반대편 언덕으로 튀어 올라가고, 그 다음엔 더 멀리 튀고, 결국 무한대로 발산합니다. 수정은 간단합니다 — 학습률을 0.01 정도로 낮추면 됩니다.
💡 경험칙: Adam은 0.001, SGD는 0.01부터 시작합니다. 발산하면 10배 줄이고, 너무 느리면 3배 키워보세요.
🔧 실습: 최강 조합 챌린지
섹션 제목: “🔧 실습: 최강 조합 챌린지”Step 1: 챌린지 규칙
섹션 제목: “Step 1: 챌린지 규칙”다음 조건에서 가장 적은 에폭으로 검증 정확도 97%를 달성하는 조합을 찾습니다.
- 데이터: MNIST (손글씨 숫자)
- 모델: 은닉층 2개짜리 Dense 네트워크
- 바꿀 수 있는 것: 활성화 함수, 옵티마이저, 학습률, 배치 크기
- 바꿀 수 없는 것: 층 수, 뉴런 수(128, 128), 출력층 Softmax, 손실 함수(sparse_categorical_crossentropy)
Step 2: 베이스라인 코드
섹션 제목: “Step 2: 베이스라인 코드”import tensorflow as tffrom 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.0x_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.9758Step 3: 실험 기록지
섹션 제목: “Step 3: 실험 기록지”팀별로 최소 4가지 조합을 시도하고 기록합니다.
| 시도 | 활성화 | 옵티마이저 | 학습률 | 배치 | 97% 도달 에폭 | 메모 |
|---|---|---|---|---|---|---|
| 1 | ReLU | Adam | 0.001 | 128 | 베이스라인 | |
| 2 | ||||||
| 3 | ||||||
| 4 |
💡 힌트: 배치 크기를 줄이면(32, 64) 에폭당 업데이트 횟수가 늘어 빨리 수렴하는 경우가 많습니다. 하지만 너무 작으면 불안정해집니다.
🤔 토론 활동 — “엔진 선택 위원회”
섹션 제목: “🤔 토론 활동 — “엔진 선택 위원회””활동 유형: 모둠(4인) · 10분
여러분은 새 AI 프로젝트의 엔진 선택 위원회입니다. 아래 세 가지 상황에 대해 “활성화-손실-옵티마이저” 조합을 결정하고 이유를 정리하세요.
- 상황 A: 아파트 가격을 예측하는 모델. 출력은 실수값(단위: 만원).
- 상황 B: 환자의 엑스레이 사진으로 폐렴/정상을 판별하는 모델.
- 상황 C: 꽃 사진을 장미·튤립·해바라기·백합·국화 5종류로 분류하는 모델.
참고 답안
| 상황 | 출력 활성화 | 손실 함수 | 옵티마이저 |
|---|---|---|---|
| A (회귀) | 없음 | MSE | Adam(0.001) |
| B (이진 분류) | Sigmoid | Binary Cross-Entropy | Adam(0.001) |
| C (다중 분류) | Softmax(5) | Sparse Categorical Cross-Entropy | Adam(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 tffrom 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.0x_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 탐지기
섹션 제목: “도전: 죽은 ReLU 탐지기”ReLU의 단점 중 하나는 죽은 뉴런(dead neuron) 입니다. 입력이 항상 음수가 되어버려 출력이 계속 0이고 기울기도 0이라 영원히 학습되지 않는 뉴런입니다. 학습이 끝난 모델의 첫 번째 은닉층에서, 훈련 데이터 전체에 대해 한 번도 0보다 큰 출력을 내지 않은 뉴런의 개수를 세는 코드를 작성하세요.
힌트
tf.keras.Model(inputs=model.input, outputs=model.layers[0].output) 으로 중간층 출력을 얻을 수 있습니다. 이 출력에 대해 각 뉴런별로 최댓값을 구하고, 그 최댓값이 0 이하인 뉴런을 세면 됩니다.
정답 코드
import numpy as npimport tensorflow as tffrom 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이 되어 멈췄다고 합니다. 원인으로 무엇을 의심해볼 수 있고, 어떻게 수정하도록 조언하시겠습니까? 세 가지 이상의 가능성을 들어 설명하세요.
예시 답안
- 학습률이 너무 크다 — 가장 흔한 원인입니다. SGD에서 lr=1.0 이상, Adam에서 lr=0.1 이상이면 가중치가 발산하여 NaN이 됩니다. 학습률을 10배씩 줄여가며(예: 0.001 → 0.0001) 재시도합니다.
- 입력 데이터를 정규화하지 않았다 — 픽셀값 0-255를 그대로 넣으면 활성화값이 폭발합니다.
x / 255.0으로 0-1 범위로 스케일링해야 합니다. - 손실 함수와 출력층 활성화 불일치 — 예컨대 출력층에 Softmax를 쓰면서
from_logits=True옵션으로 손실을 계산하면 log(0)이 발생하여 NaN이 될 수 있습니다. 출력층 활성화와 손실 함수 설정을 재확인해야 합니다.
추가로 배치 정규화 누락, 초기값이 과도하게 큰 경우(표준편차 큰 초기화) 등도 원인이 될 수 있습니다.
자기점검 체크리스트
- Sigmoid, Tanh, ReLU, Softmax를 용도별로 구분해서 말할 수 있다
- 분류 문제에 MSE 대신 Cross-Entropy를 쓰는 이유를 설명할 수 있다
- SGD, Momentum, Adam의 차이를 실험 결과와 함께 설명할 수 있다
- 학습률이 너무 크거나 작을 때 생기는 증상을 구분할 수 있다
- 오늘의 질문(“같은 모델, 다른 운명”)에 나만의 답을 말할 수 있다
🔗 참고 자료
섹션 제목: “🔗 참고 자료”📎 Keras 공식 옵티마이저 문서
섹션 제목: “📎 Keras 공식 옵티마이저 문서”tf.keras.optimizers 모듈의 전체 목록과 각 옵티마이저의 하이퍼파라미터 설명을 확인할 수 있습니다 →
📎 CS231n 강의 노트: Neural Networks Part 1
섹션 제목: “📎 CS231n 강의 노트: Neural Networks Part 1”활성화 함수와 기울기 소실을 그림과 함께 깊이 있게 설명하는 스탠포드 강의 자료입니다 →
📎 Distill — “Why Momentum Really Works”
섹션 제목: “📎 Distill — “Why Momentum Really Works””Momentum이 실제로 어떻게 작동하는지 인터랙티브 시각화로 설명하는 읽을거리입니다 →
💭 성찰
섹션 제목: “💭 성찰”오늘 수업에서 배운 것을 돌아보며 아래 질문에 스스로 답해봅니다.
- 세 가지 부품(활성화·손실·옵티마이저) 중 가장 인상 깊었던 것은 무엇이며, 그 이유는?
- 옵티마이저 경주 실험에서 예상과 달랐던 결과가 있었습니까? 있다면 무엇이었습니까?
- 만약 이 중 하나의 설정을 잘못 골랐을 때 모델에 어떤 증상이 나타날지, 내 언어로 설명할 수 있습니까?
- 오늘 배운 내용을 친구에게 1분 안에 설명한다면 어떤 비유를 쓰겠습니까?
🔗 다음 차시 미리보기
섹션 제목: “🔗 다음 차시 미리보기”다음 차시에서는 배치 정규화와 드롭아웃 을 다룹니다. 학습은 되는데 검증 정확도가 오르지 않는 과적합을 해결하는 기법들입니다. 오늘 만든 최강 조합에 정규화 기법을 얹어, 훈련과 검증 성능의 간격을 줄이는 실험을 이어갑니다.
생각해볼 질문: “같은 반 친구와 같은 책으로 공부했는데, 왜 어떤 친구는 시험 문제만 외우고 실전에서는 틀릴까요? 모델도 똑같은 일이 일어납니다.”