딥러닝은 어떻게 배우는가
이 챕터에서 배우는 것
1. 뉴런 하나가 값을 계산하는 과정(가중합 + 편향 → 활성화 함수)
2. relu·sigmoid·softmax의 출력 범위와 각각을 쓰는 자리
3. 순전파와 역전파가 한 epoch 안에서 반복되는 흐름시험은 이론을 글로 묻지 않습니다. 대신 "은닉층 2개 이상의 모델을 만드시오", "출력층을 문제에 맞게 설계하시오" 같은 지시가 나옵니다. 이 챕터의 개념을 알고 있어야 다음 챕터부터 나오는 코드 한 줄 한 줄이 무엇을 정하는 것인지 보입니다.
이 챕터에서는 Keras를 아직 쓰지 않습니다. 딥러닝의 계산은 numpy 몇 줄이면 손으로 재현할 수 있을 만큼 단순합니다. 그 단순한 계산을 먼저 눈으로 확인한 뒤, 다음 챕터에서 같은 것을 Keras 코드로 옮깁니다.
3.1 뉴런 하나가 하는 일
한 줄 요약 — 뉴런은 입력에 가중치를 곱해 모두 더하고, 편향을 더한 뒤, 활성화 함수를 한 번 통과시킨 값을 내보냅니다.
뉴런(neuron)은 딥러닝 모델을 이루는 최소 계산 단위입니다. 하는 일은 두 단계뿐입니다.
- 가중합 + 편향: 입력값 각각에 가중치(weight)를 곱해 전부 더하고, 편향(bias) 한 개를 더합니다. 결과를
z라고 부릅니다. - 활성화:
z를 활성화 함수(activation function)에 넣어 최종 출력a를 만듭니다.
여기서 가중치는 "이 입력을 얼마나 중요하게 볼 것인가"를 나타내는 숫자이고, 편향은 "기본적으로 얼마만큼 얹고 시작할 것인가"를 나타내는 숫자입니다. 이 두 종류의 숫자가 바로 학습으로 바뀌는 값, 즉 모델의 파라미터입니다. 입력 데이터도 정답도 바뀌지 않습니다. 학습 중에 바뀌는 것은 오직 가중치와 편향입니다.
뉴런 하나를 numpy로 그대로 계산해 봅니다.
import numpy as np
x = np.array([2.0, 3.0, 1.0]) # 입력 3개
w = np.array([0.5, -1.0, 2.0]) # 각 입력에 붙은 가중치 3개
b = 0.1 # 편향 1개
z = np.dot(w, x) + b # 1단계: 가중합 + 편향
a = np.maximum(0, z) # 2단계: 활성화(relu)
print(f"z = {z:.2f}, a = {a:.2f}")
# 가중치를 바꾸면 z가 음수가 되고, relu는 그것을 0으로 막습니다
w2 = np.array([0.5, -1.0, -2.0])
z2 = np.dot(w2, x) + b
print(f"z2 = {z2:.2f}, a2 = {np.maximum(0, z2):.2f}")
출력 요지: 첫 줄은 z = 0.10, a = 0.10, 둘째 줄은 z2 = -3.90, a2 = 0.00이 나옵니다. z가 음수일 때 relu가 0으로 잘라내는 동작을 확인할 수 있습니다.
⚠️ 시험 포인트 1 — 파라미터는 가중치와 편향뿐입니다
다음 챕터에서model.summary()가 출력하는 "Param #"은 이 가중치와 편향의 개수를 센 값입니다. 층의 노드 수를 바꾸면 이 숫자가 왜 달라지는지 여기서 이해해 두면 Ch04의 파라미터 계산이 그대로 따라옵니다.
3.2 뉴런을 나란히·겹겹이 쌓으면 층이 됩니다
한 줄 요약 — 같은 입력을 받는 뉴런을 옆으로 나란히 놓으면 하나의 층이고, 층의 출력을 다음 층의 입력으로 넘기면 깊은 신경망이 됩니다.
층은 세 종류로 부릅니다.
| 층 | 역할 | 노드 수를 정하는 기준 |
|---|---|---|
| 입력층 | 데이터가 들어오는 자리 | 특성(컬럼) 개수로 고정 |
| 은닉층 | 입력을 조합해 특징을 만들어 내는 중간 층 | 자유롭게 선택(예: 64, 32) |
| 출력층 | 최종 답을 내놓는 층 | 문제 유형이 결정(Ch04) |
입력층과 출력층의 크기는 우리가 정하는 것이 아니라 데이터와 문제가 정해 줍니다. 컬럼이 8개면 입력은 8이고, 이진분류면 출력은 1입니다. 반면 은닉층의 개수와 노드 수는 설계자가 고르는 값입니다.
층 전체의 계산은 뉴런 하나의 계산을 행렬 곱으로 묶은 것뿐입니다. 입력 3개를 받아 은닉 노드 4개를 만드는 층을 계산해 봅니다.
import numpy as np
rng = np.random.default_rng(42)
x = np.array([2.0, 3.0, 1.0]) # 입력 3개
W1 = rng.normal(size=(3, 4)) # 가중치 행렬: (입력 수, 노드 수)
b1 = np.zeros(4) # 편향은 노드마다 1개씩
h = np.maximum(0, x @ W1 + b1) # 은닉층 4개 노드의 출력
print("가중치 행렬 shape:", W1.shape)
print("은닉층 출력 shape:", h.shape)
print("음수가 하나도 없는가?:", bool((h >= 0).all()))
출력 요지: 가중치 행렬은 (3, 4), 은닉층 출력은 (4,)이고, relu를 통과했으므로 음수가 하나도 없습니다(True).
여기서 기억할 규칙은 하나입니다. 가중치 행렬의 모양은 (앞 층의 노드 수, 뒤 층의 노드 수)이고, 편향은 뒤 층의 노드 수만큼 있습니다. 그래서 이 층의 파라미터 개수는 3 × 4 + 4 = 16개입니다. 층을 더 쌓고 싶으면 h를 다음 층의 입력으로 넣어 같은 계산을 반복하면 됩니다.
3.3 활성화 함수 — 어디에 무엇을 쓰는가
한 줄 요약 — 은닉층에는 relu, 이진분류 출력에는 sigmoid, 다중분류 출력에는 softmax를 씁니다.
| 활성화 함수 | 출력 범위 | 쓰는 자리 | 의미 |
|---|---|---|---|
relu |
0 이상 | 은닉층 기본값 | 음수는 0으로, 양수는 그대로 |
sigmoid |
0 ~ 1 (값 1개) | 이진분류 출력층 | "1일 확률" |
softmax |
0 ~ 1, 합이 1 (값 K개) | 다중분류 출력층 | "각 클래스일 확률" |
| 없음 | 제한 없음 | 회귀 출력층 | 예측한 숫자 그 자체 |
세 함수를 직접 계산해 확인합니다.
import numpy as np
def relu(z):
return np.maximum(0, z)
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def softmax(z):
return np.exp(z) / np.exp(z).sum()
z = np.array([1.0, 2.0, 0.5])
p = softmax(z)
print("relu :", np.round(relu(z), 3))
print("sigmoid:", np.round(sigmoid(z), 3))
print("softmax:", np.round(p, 3))
print("softmax 합:", round(float(p.sum()), 6))
출력 요지: relu는 [1. 2. 0.5], sigmoid는 [0.731 0.881 0.622](각각 독립적으로 0~1), softmax는 [0.231 0.629 0.14 ]이고 합은 정확히 1.0입니다. sigmoid 세 값의 합은 1이 아니지만 softmax는 1이라는 점이 두 함수의 결정적 차이입니다.
참고: 실무에서는
z가 아주 큰 값일 때np.exp가 넘치는 것을 막기 위해np.exp(z - z.max())로 바꿔 쓰기도 합니다. 계산 결과는 같습니다. 시험에서는 Keras가 내부적으로 처리하므로activation='softmax'라고만 쓰면 됩니다.
활성화 함수가 없으면 무슨 일이 생기는가
은닉층에서 활성화 함수를 빼면, 층을 아무리 많이 쌓아도 전체가 하나의 직선(선형식)으로 합쳐집니다. 곱하고 더하는 연산만 반복한 결과는 결국 곱하고 더한 식 하나이기 때문입니다. 딥러닝이 복잡한 패턴을 배울 수 있는 이유는 층 사이마다 relu 같은 꺾인 함수가 끼어들어 직선을 구부려 주기 때문입니다.
⚠️ 시험 포인트 2 — 은닉층에 sigmoid를 남용하지 않습니다
sigmoid는 출력을 0~1로 눌러 담기 때문에 층을 여러 개 쌓으면 신호가 점점 작아져 학습이 잘 진행되지 않습니다. 은닉층은 relu가 표준이고, sigmoid는 이진분류 출력층 자리에 씁니다.⚠️ 시험 포인트 3 — 이진분류 출력층에 softmax를 쓰지 않습니다
Dense(1, activation='softmax')는 값이 하나뿐이라 항상 1.0을 출력합니다. 학습이 전혀 되지 않는데도 오류 메시지가 나지 않아 알아채기 어렵습니다. 이진분류는sigmoid로 노드 1개, 다중분류는softmax로 노드 K개입니다. 출력층은 Ch04에서, 손실 함수와의 짝은 Ch05에서 확정합니다.
3.4 순전파와 역전파 — 예측하고, 되돌아가 고칩니다
한 줄 요약 — 순전파는 왼쪽에서 오른쪽으로 가며 예측값을 만들고, 역전파는 오른쪽에서 왼쪽으로 돌아오며 각 가중치를 얼마나 고칠지 알려 줍니다.
순전파(forward propagation) 는 지금까지 본 계산 그대로입니다. 입력 → 은닉층 → 은닉층 → 출력층 순서로 값을 흘려보내면 예측값이 나옵니다.
예측값이 나오면 정답과 비교해 손실 함수(loss function) 로 오차를 하나의 숫자로 잽니다. 이 숫자가 작을수록 좋은 모델입니다. 어떤 손실 함수를 쓰는지는 문제 유형에 따라 달라지며, Ch05에서 정리합니다.
역전파(backpropagation) 는 이 손실값에서 출발해 거꾸로 거슬러 가며 "가중치 하나하나가 오차에 얼마나 기여했는지" 를 계산합니다. 마지막 층부터 앞 층으로 책임을 나눠 전달하기 때문에 "역"전파라고 부릅니다. 계산된 기여도(기울기)를 보고 손실이 줄어드는 방향으로 모든 가중치를 조금씩 옮기는 것이 학습입니다. 이때 "조금씩"의 보폭을 정하는 값이 학습률(learning rate)이며, 이 갱신을 담당하는 것이 Ch05에서 배울 optimizer입니다.
역전파의 수식 유도는 AICE Associate 시험 범위 밖입니다. 시험에서 필요한 것은 "손실이 줄어드는 방향으로 가중치가 자동으로 수정된다" 는 방향 감각과, 그 과정이 model.fit() 한 줄 안에서 벌어진다는 사실입니다.
3.5 학습 루프 — step과 epoch
한 줄 요약 — 데이터 한 묶음을 넣고 예측·채점·수정하는 한 바퀴가 1 step이고, 전체 데이터를 한 번 다 도는 것이 1 epoch입니다.
학습은 위 그림의 다섯 단계를 수없이 반복하는 일입니다.
- 데이터를
batch_size만큼 잘라 한 묶음 넣습니다 - 순전파로 예측합니다
- 손실 함수로 오차를 잽니다
- 역전파로 가중치별 수정 방향을 구합니다
- 가중치를 그 방향으로 조금 옮깁니다 → 다시 1번으로
여기서 두 용어를 정확히 구분해야 합니다.
- step: 위 다섯 단계를 한 바퀴 돈 것. 한 묶음(batch)당 한 번 가중치가 갱신됩니다.
- epoch: 학습 데이터 전체를 한 번 다 사용한 것. 데이터가 1,000행이고
batch_size=32라면 1 epoch은 32 step으로 이루어집니다. 1,000을 32로 나누면 31.25인데, 남는 8행도 마지막 한 묶음으로 학습에 쓰이므로 올림해서 32 step이 됩니다.
epochs=50이라고 쓰면 전체 데이터를 50번 반복해서 본다는 뜻이고, 그동안 가중치는 50 × 32 = 1,600번 갱신됩니다. 이 인자들의 설정 방법은 Ch05에서 다룹니다.
⚠️ 시험 포인트 4 — epoch과 batch_size를 헷갈리지 않습니다
epochs는 "전체 데이터를 몇 번 반복할지",batch_size는 "한 번에 몇 행씩 넣을지"입니다. 두 값을 바꿔 넣으면 오류는 나지 않지만 학습 시간이 비정상적으로 길어지거나 성능이 나오지 않습니다.
3.6 층을 몇 개, 노드를 몇 개 둘 것인가
한 줄 요약 — 층과 노드를 늘리면 표현력이 커지지만 과적합 위험도 함께 커집니다.
- 너무 작으면: 데이터의 패턴을 담아내지 못해 학습 데이터에서조차 성능이 낮습니다(과소적합).
- 너무 크면: 학습 데이터를 통째로 외워 버려 학습 성능만 좋고 평가(테스트) 데이터 성능은 떨어집니다(과적합). 진단과 대응은 Ch06에서 다룹니다.
시험에서는 최적 구조를 찾는 실험을 요구하지 않습니다. 요구 사항은 대개 "은닉층 2개 이상" 같은 형태입니다. 아래 정도를 기본형으로 외워 두면 대부분의 문항에 무리 없이 대응할 수 있습니다.
| 항목 | 시험장 기본값 |
|---|---|
| 은닉층 수 | 2~3개 (지시가 있으면 지시 우선) |
| 노드 수 | 앞 층부터 64 → 32 처럼 점점 줄이기 |
| 은닉층 활성화 | 전부 relu |
| 출력층 | 문제 유형에 맞춰 결정 (Ch04) |
시험장 표준 코드
Keras 없이 numpy만으로 2층 신경망의 순전파 → 손실 계산 → "가중치를 흔들면 손실이 변한다" 까지를 한 번에 확인하는 코드입니다. model.fit()이 내부에서 수천 번 반복하는 일의 축소판입니다.
import numpy as np
rng = np.random.default_rng(42)
# 1) 데이터: 특성 4개짜리 샘플 6개, 정답은 0/1
X = rng.normal(size=(6, 4))
y = np.array([0, 1, 1, 0, 1, 0])
# 2) 파라미터 초기화: 입력 4 -> 은닉 5 -> 출력 1
W1 = rng.normal(size=(4, 5)) * 0.5
b1 = np.zeros(5)
W2 = rng.normal(size=(5, 1)) * 0.5
b2 = np.zeros(1)
def relu(z):
return np.maximum(0, z)
def sigmoid(z):
return 1 / (1 + np.exp(-z))
# 3) 순전파: 은닉층은 relu, 이진분류 출력층은 sigmoid
def forward(X, W1, b1, W2, b2):
h = relu(X @ W1 + b1)
out = sigmoid(h @ W2 + b2)
return out.ravel()
# 4) 손실 함수: 이진 교차 엔트로피(binary_crossentropy와 같은 계산)
def binary_crossentropy(y_true, y_pred):
y_pred = np.clip(y_pred, 1e-7, 1 - 1e-7) # log(0) 방지
return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
pred = forward(X, W1, b1, W2, b2)
loss = binary_crossentropy(y, pred)
print("예측 확률:", np.round(pred, 3))
print("손실:", round(float(loss), 4))
# 5) 가중치 하나만 살짝 키워 보고 손실 변화를 봅니다
W2_up = W2.copy()
W2_up[0, 0] += 0.1
loss_up = binary_crossentropy(y, forward(X, W1, b1, W2_up, b2))
print("W2[0,0] += 0.1 이후 손실:", round(float(loss_up), 4))
print("손실 변화량:", round(float(loss_up - loss), 4))
출력 요지:
- 예측 확률은 6개 값이 모두 0과 1 사이에 있습니다(sigmoid의 출력 범위).
- 손실은 0.7 안팎의 값입니다. 아직 학습을 하지 않아 예측이 절반 확률에 가깝기 때문입니다.
- 손실 변화량은 이 실행에서 0.0019, 즉 양수입니다(손실: 0.708 → W2[0,0] += 0.1 이후 손실: 0.7099). 가중치를 키웠더니 손실이 오히려 늘었다는 뜻이므로, W2[0, 0]은 키우는 쪽이 아니라 줄이는 쪽으로 옮겨야 합니다.
- 일반화하면, 변화량이 음수면 그 방향으로 가중치를 옮기는 것이 이득이고, 양수면 반대 방향으로 옮겨야 합니다.
마지막 출력이 이 챕터의 결론입니다. 역전파는 이 "어느 방향으로 옮겨야 손실이 줄어드는가"를 가중치 하나씩 흔들어 보지 않고 전부 한 번에 계산해 주는 방법이고, optimizer는 그 방향으로 실제로 값을 옮기는 담당자입니다. 우리가 Keras에서 쓰게 될 코드는 이 전 과정을 model.fit() 한 줄로 대신합니다.
핵심 요약
| 개념 | 한 줄 정리 |
|---|---|
| 뉴런 | z = 가중합 + 편향 → a = 활성화(z). 학습으로 바뀌는 값은 가중치와 편향뿐 |
| 층 | 입력층 = 특성 수 고정 / 은닉층 = 설계자 선택 / 출력층 = 문제 유형이 결정 |
| 활성화 함수 | 은닉층 relu, 이진 출력 sigmoid(0~1), 다중 출력 softmax(합 1), 회귀 출력은 활성화 없음 |
| 순전파·역전파 | 순전파로 예측 → 손실 계산 → 역전파로 수정 방향 계산 → 가중치 갱신 |
| step / epoch | 배치 한 묶음 처리 = 1 step, 전체 데이터 1회 통과 = 1 epoch |
| 구조 설계 | 크면 표현력↑·과적합 위험↑. 시험은 "은닉층 2개 이상 + relu + 64→32"면 충분 |
다음 챕터에서는 이 구조를 Sequential과 Dense로 옮겨 실제로 동작하는 모델을 만듭니다.
Ch03 확인 문제 — 딥러닝은 어떻게 배우는가
본 문제는 공개된 출제 기준에 따른 유형 연습 문제입니다. 기출 원문이 아닙니다.
이 챕터는 Keras를 사용하지 않습니다. 모든 코드 문항은 numpy만으로 풀 수 있습니다.
총 5문항 (객관식 3 / 코드 빈칸 2), 권장 소요 시간 15분.
문제
Q1. (객관식 · 기본) 활성화 함수의 출력 범위와 쓰는 자리
다음 중 활성화 함수와 그 출력 범위, 쓰는 자리를 모두 옳게 짝지은 것은?
relu— 출력 범위 −1 ~ 1 — 이진분류 출력층sigmoid— 출력 범위 0 ~ 1 (값 1개) — 이진분류 출력층softmax— 출력 범위 0 이상 (상한 없음) — 은닉층 기본값- 활성화 함수 없음 — 출력 범위 0 ~ 1 — 다중분류 출력층
sigmoid— 출력 범위 0 이상 (상한 없음) — 은닉층 기본값
Q2. (객관식 · 기본) 노드 1개에 softmax를 씌우면
한 수험생이 이진분류 모델의 출력층을 노드 1개 + softmax로 만들었습니다.
실행해도 오류 메시지는 나오지 않았지만, epoch을 아무리 늘려도 정확도가 전혀 오르지 않았습니다.
원인을 가장 정확히 설명한 것은?
softmax는 은닉층 전용 함수여서 출력층에서는 계산 자체가 수행되지 않는다.softmax는 값이 하나뿐일 때 입력값과 관계없이 항상 1.0을 내놓기 때문에, 예측이 입력을 전혀 반영하지 못한다.softmax는 음수 입력을 0으로 잘라내므로 데이터의 절반이 버려진다.softmax는 출력의 합을 0으로 맞추므로 확률로 해석할 수 없다.- 노드가 1개인 층에는 편향이 만들어지지 않아 학습할 파라미터가 없다.
Q3. (객관식 · 응용) step과 epoch
학습 데이터가 2,000행이고 batch_size=50, epochs=10으로 학습했습니다.
학습이 끝날 때까지 가중치가 갱신되는 총 횟수는 몇 번입니까? (마지막 배치도 1 step으로 셉니다)
- 10회
- 40회
- 50회
- 400회
- 20,000회
Q4. (코드 빈칸 · 기본) 뉴런 하나의 계산
입력 3개를 받는 뉴런 하나가 값을 계산하는 코드입니다.
(가)에는 가중합 + 편향을, (나)에는 relu 활성화를 채우십시오.
단, 두 빈칸 모두 numpy 함수를 사용해 한 줄로 작성하십시오.
import numpy as np
x = np.array([1.0, -2.0, 4.0]) # 입력 3개
w = np.array([0.5, 1.5, -0.25]) # 가중치 3개
b = 0.4 # 편향 1개
z = ______(가)______
a = ______(나)______
print(f"z = {z:.2f}")
print(f"a = {a:.2f}")
추가로, 위 코드의 출력 a 값은 얼마입니까? 그 값이 나온 이유도 한 줄로 쓰십시오.
Q5. (코드 빈칸 · 응용) 2층 신경망의 순전파
특성 4개짜리 샘플 6개를 입력받아, 은닉층 노드 5개를 거쳐 이진분류 확률 1개를 내놓는 순전파 코드입니다.
(가)에는 두 번째 가중치 행렬의 size 튜플을, (나)·(다)에는 각 위치에 맞는 활성화 함수 이름을 채우십시오.
import numpy as np
def relu(z):
return np.maximum(0, z)
def sigmoid(z):
return 1 / (1 + np.exp(-z))
rng = np.random.default_rng(42)
X = rng.normal(size=(6, 4)) # 샘플 6개, 특성 4개
W1 = rng.normal(size=(4, 5))
b1 = np.zeros(5)
W2 = rng.normal(size=___(가)___) # 은닉 5개 -> 출력 1개
b2 = np.zeros(1)
h = ___(나)___(X @ W1 + b1) # 은닉층
out = ___(다)___(h @ W2 + b2) # 출력층
print("h.shape :", h.shape)
print("out.shape :", out.shape)
print("예측 확률 :", np.round(out.ravel(), 3))
추가로, 이 모델 전체의 파라미터(가중치 + 편향) 개수를 계산식과 함께 쓰십시오.
해답 및 해설 보기
Q1 해답
정답: 2번
정답 코드 (실증 예제)
import numpy as np
def relu(z): return np.maximum(0, z)
def sigmoid(z): return 1 / (1 + np.exp(-z))
def softmax(z): return np.exp(z) / np.exp(z).sum()
z = np.array([-2.0, 0.5, 3.0])
print("relu :", np.round(relu(z), 3), "-> 최솟값", relu(z).min())
print("sigmoid:", np.round(sigmoid(z), 3), "-> 합", round(float(sigmoid(z).sum()), 4))
print("softmax:", np.round(softmax(z), 3), "-> 합", round(float(softmax(z).sum()), 6))
relu : [0. 0.5 3. ] -> 최솟값 0.0
sigmoid: [0.119 0.622 0.953] -> 합 1.6942
softmax: [0.006 0.075 0.918] -> 합 1.0
왜 이렇게 푸는가
세 함수는 "출력 범위"만 보면 바로 구분됩니다.
relu는 음수를 0으로 자르므로 0 이상이고 상한이 없습니다. 값을 왜곡 없이 통과시키므로 은닉층 기본값입니다.sigmoid는 어떤 실수를 넣어도 0 ~ 1 사이 값 하나를 냅니다. 그래서 "1일 확률" 하나가 필요한 이진분류 출력층에 맞습니다. 2번이 이 조합입니다.softmax는 K개 값을 받아 합이 1인 확률 분포로 만듭니다. "여러 클래스 중 하나"를 고르는 다중분류 출력층 자리입니다.- 활성화 함수를 쓰지 않으면 값의 범위 제한이 사라지므로 회귀 출력층에 씁니다.
위 실행 결과에서 sigmoid 세 값의 합은 1.69로 1이 아니고, softmax는 정확히 1.0입니다. 이 한 줄이 두 함수를 가르는 결정적 차이입니다.
자주 틀리는 지점
- 은닉층에 sigmoid를 쓰는 것(5번 유형). 오류는 나지 않지만 층을 쌓을수록 신호가 0~1로 계속 눌려 학습이 잘 진행되지 않습니다. 은닉층은
relu가 표준입니다. sigmoid와softmax를 "둘 다 확률이니 아무거나"로 취급하는 것. 값이 1개(sigmoid)냐 K개·합 1(softmax)이냐가 다르고, 이 차이가 출력층 노드 수를 결정합니다.
Q2 해답
정답: 2번
정답 코드 (실증 예제)
import numpy as np
def softmax(z): return np.exp(z) / np.exp(z).sum()
def sigmoid(z): return 1 / (1 + np.exp(-z))
for raw in [np.array([-5.0]), np.array([0.0]), np.array([12.7])]:
print(f"z={raw[0]:6.1f} -> softmax={softmax(raw)}, sigmoid={np.round(sigmoid(raw), 4)}")
z= -5.0 -> softmax=[1.], sigmoid=[0.0067]
z= 0.0 -> softmax=[1.], sigmoid=[0.5 ]
z= 12.7 -> softmax=[1.], sigmoid=[1. ]
왜 이렇게 푸는가
softmax는 exp(z) / (모든 exp(z)의 합)입니다. 값이 하나뿐이면 분자와 분모가 같은 수가 되어 결과는 언제나 1.0입니다. z가 −5든 12.7이든 출력이 바뀌지 않습니다.
출력이 입력에 반응하지 않으므로, 순전파 결과가 정답과 아무 관계가 없고 역전파가 알려 줄 수정 방향도 사실상 사라집니다. 그래서 오류 메시지 없이 정확도만 제자리에 머무는 가장 찾기 어려운 실수가 됩니다.
같은 z 값에 sigmoid를 씌우면 0.0067 / 0.5 / 1.0으로 입력에 따라 달라집니다. 이진분류 출력층은 이 성질이 필요합니다.
자주 틀리는 지점
- 이진분류에
Dense(1, activation='softmax'). 문법 오류가 없어 실행은 됩니다. "이진분류 = sigmoid + 노드 1개 / 다중분류 = softmax + 노드 K개"를 한 쌍으로 외우십시오. - 반대로 다중분류 출력층에 sigmoid를 쓰는 것도 같은 계열의 실수입니다. 클래스별 확률의 합이 1이 되지 않아 "가장 확률 높은 클래스"를 고르는 근거가 흔들립니다.
Q3 해답
정답: 4번 (400회)
정답 코드 (실증 예제)
import numpy as np
n_rows, batch_size, epochs = 2000, 50, 10
steps_per_epoch = int(np.ceil(n_rows / batch_size))
total_updates = steps_per_epoch * epochs
print("1 epoch당 step 수:", steps_per_epoch)
print("총 가중치 갱신 횟수:", total_updates)
1 epoch당 step 수: 40
총 가중치 갱신 횟수: 400
왜 이렇게 푸는가
가중치는 step마다 한 번 갱신됩니다. 그러므로 답은 1 epoch당 step 수 × epochs입니다.
- 1 epoch당 step 수 =
ceil(전체 행 수 / batch_size)=ceil(2000 / 50)= 40 - 총 갱신 횟수 = 40 × 10 = 400
용어를 다시 정리하면, batch_size는 "한 번에 몇 행을 넣는가", epochs는 "전체 데이터를 몇 번 반복하는가"입니다. step은 그 사이에서 자동으로 결정되는 값이며 우리가 직접 지정하지 않습니다.
자주 틀리는 지점
- 5번(20,000회)을 고르는 실수 — "샘플 하나마다 가중치가 갱신된다"고 오해한 경우입니다. 갱신 단위는 샘플이 아니라 배치입니다.
epochs와batch_size를 바꿔 넣는 실수. 예를 들어epochs=32, batch_size=50을 의도했는데 값을 뒤집어 쓰면 오류는 나지 않지만 학습 시간이나 성능이 의도와 크게 달라집니다.- 나누어떨어지지 않을 때 소수점을 버리는 실수. 마지막 자투리 배치도 1 step이므로
ceil로 올림합니다.
Q4 해답
정답 코드
import numpy as np
x = np.array([1.0, -2.0, 4.0])
w = np.array([0.5, 1.5, -0.25])
b = 0.4
z = np.dot(w, x) + b # (가) 가중합 + 편향
a = np.maximum(0, z) # (나) relu 활성화
print(f"z = {z:.2f}")
print(f"a = {a:.2f}")
# 편향을 4.0으로 바꾸면 z가 양수가 되어 relu가 값을 그대로 통과시킵니다
b2 = 4.0
z2 = np.dot(w, x) + b2
print(f"b=4.0일 때 z = {z2:.2f}, a = {np.maximum(0, z2):.2f}")
z = -3.10
a = 0.00
b=4.0일 때 z = 0.50, a = 0.50
a의 값과 이유: a = 0.00입니다. 가중합 결과 z = 0.5×1.0 + 1.5×(−2.0) + (−0.25)×4.0 + 0.4 = −3.1로 음수이고, relu는 음수를 전부 0으로 잘라내기 때문입니다.
왜 이렇게 푸는가
뉴런 하나의 계산은 딱 두 줄입니다.
np.dot(w, x) + b— 입력마다 가중치를 곱해 모두 더한 뒤(내적) 편향 하나를 더합니다.w와x가 같은 길이의 1차원 배열이므로np.dot이 곧 가중합입니다.(w * x).sum() + b나w @ x + b로 써도 결과는 같습니다.np.maximum(0, z)— relu입니다.max(0, z)가 아니라np.maximum을 쓰는 이유는, 뒤에서z가 배열(층 전체 출력)이 되어도 원소별로 동작해야 하기 때문입니다.
여기서 학습으로 값이 바뀌는 것은 w와 b뿐입니다. x(입력)는 데이터이고 바뀌지 않습니다.
자주 틀리는 지점
np.dot(w, x)에 편향을 더하는 것을 빠뜨리는 것. 편향은 노드마다 하나씩 반드시 있으며, Ch04의model.summary()에서 Param 수가(입력 수 × 노드 수) + 노드 수인 이유가 바로 이+ b입니다.- relu를
max(0, z)로 쓰는 것. 파이썬 내장max는 배열이 들어오면 "The truth value of an array..." 오류를 냅니다. 배열에는np.maximum을 씁니다. z가 음수인데a도 음수로 답하는 것. relu의 출력은 절대 음수가 되지 않습니다.
Q5 해답
정답 코드
import numpy as np
def relu(z):
return np.maximum(0, z)
def sigmoid(z):
return 1 / (1 + np.exp(-z))
rng = np.random.default_rng(42)
X = rng.normal(size=(6, 4))
W1 = rng.normal(size=(4, 5))
b1 = np.zeros(5)
W2 = rng.normal(size=(5, 1)) # (가) (앞 층 노드 수, 뒤 층 노드 수)
b2 = np.zeros(1)
h = relu(X @ W1 + b1) # (나) 은닉층 -> relu
out = sigmoid(h @ W2 + b2) # (다) 이진 출력층 -> sigmoid
print("h.shape :", h.shape)
print("out.shape :", out.shape)
print("예측 확률 :", np.round(out.ravel(), 3))
print("모두 0~1 사이인가?:", bool(((out > 0) & (out < 1)).all()))
h.shape : (6, 5)
out.shape : (6, 1)
예측 확률 : [0.79 0.52 0.68 0.627 0.63 0.513]
모두 0~1 사이인가?: True
파라미터 개수: (4 × 5 + 5) + (5 × 1 + 1) = 25 + 6 = 31 → 31개
왜 이렇게 푸는가
- (가)
(5, 1)— 가중치 행렬의 모양은 언제나 (앞 층의 노드 수, 뒤 층의 노드 수) 입니다. 은닉층 노드가 5개, 출력이 1개이므로(5, 1)입니다. 이 규칙을 지켜야h @ W2의 행렬 곱이 성립합니다((6,5) @ (5,1) → (6,1)). - (나)
relu— 은닉층이므로 relu입니다. sigmoid를 쓰면 오류는 없지만 신호가 눌려 학습이 더뎌집니다. - (다)
sigmoid— 이진분류 출력층이므로 0~1 확률 하나를 내는 sigmoid입니다. 출력 확률 6개가 모두 0과 1 사이에 있는 것으로 확인됩니다.
파라미터 개수는 층마다 (앞 노드 수 × 뒤 노드 수) + 뒤 노드 수입니다. 뒤에 더하는 항이 편향이며, Ch04의 model.summary()가 출력하는 Param # 와 정확히 같은 계산입니다.
자주 틀리는 지점
- 가중치 행렬의 모양을 뒤집어
(1, 5)로 쓰는 것.h @ W2에서 곧바로 shape 불일치 오류가 납니다. "앞 → 뒤" 순서를 고정하십시오. - 편향의 크기를 앞 층 노드 수로 잡는 것. 편향은 뒤 층(자기 층)의 노드 수만큼 있습니다.
- 파라미터를 셀 때 편향을 빼고
4×5 + 5×1 = 25로 답하는 것. 편향 6개를 더해 31개입니다.