2장의 퍼셉트론에는 결정적 한계가 하나 있었다. 가중치와 편향을 사람이 손으로 정해야 했다는 것이다. AND를 만들려면 내가 앉아서 를 궁리해 넣었고, XOR는 NAND와 OR를 어떻게 조합할지까지 내가 설계했다. 우리가 진짜 원하는 것은 그 반대다 — 데이터를 주면 가중치를 스스로 찾아내는 장치. 신경망이 바로 그것이고, 이 책의 4장부터 그 학습이 시작된다.
그런데 학습으로 넘어가기 전에, 퍼셉트론의 몸을 한 군데 고쳐야 한다. 4장의 학습은 “가중치를 아주 조금 흔들면 결과가 얼마나 변하나”를 미분으로 재고, 그 방향으로 가중치를 밀어 나가는 과정이다. 그러려면 결과가 가중치에 대해 매끄럽게 반응해야 한다. 그런데 퍼셉트론의 계단 함수는 임계값에서 0에서 1로 뚝 튀고 나머지는 완전히 평평하다 — 미분이 대부분 0이고 한 점에서는 무한대다. 학습 신호가 나올 수 없는 몸이다. 이 장은 그 계단을 매끄러운 곡선으로 바꾸는 데서 시작해, 신경망의 순전파를 행렬 곱으로 세우고, 마지막에 학습된 가중치를 빌려 와 손글씨를 알아보는 데까지 간다. 학습은 아직 없다. 이 장은 **이미 배운 신경망이 어떻게 답을 내놓는지(추론)**를 밑바닥까지 분해하는 장이다.
퍼셉트론에서 신경망으로 — 활성화 함수라는 이름
2장의 퍼셉트론을 수식 하나로 다시 쓰면, 신경망이 어디서 갈라지는지가 또렷해진다. 입력에 가중치를 곱해 더하고 편향을 얹은 값을 라 하고, 그 가 0을 넘는지로 0/1을 내보냈다.
여기서 는 “가중합 를 받아 출력 로 바꾸는 함수”다. 이 에 **활성화 함수(activation function)**라는 이름을 붙이는 순간, 퍼셉트론과 신경망은 하나의 틀 안에 들어온다. 둘의 차이는 구조가 아니라 오직 가 무엇이냐다. 퍼셉트론은 가 계단 함수(임계값에서 튀는)이고, 신경망은 가 매끄러운 곡선이다.
이렇게 이름을 분리해 두면 신경망의 한 뉴런이 하는 일이 두 단계로 깔끔하게 쪼개진다. 먼저 입력을 가중합해 를 만들고(선형 부분), 그 다음 를 활성화 함수에 통과시켜 를 만든다(비선형 부분). 앞으로 모든 층은 이 “가중합 → 활성화”의 반복이다. 그러니 활성화 함수를 어떤 것으로 고르느냐가 신경망의 성격을 정한다.
판단 기준: 퍼셉트론과 신경망을 다른 것으로 외우지 말고 “활성화 함수만 바뀐 같은 구조”로 보라 — 그러면 계층 코드가 전부 가중합 → h(가중합)의 반복임이 눈에 들어온다. 함정: 활성화 함수를 “출력을 예쁘게 눌러 주는 장식”쯤으로 여기는 것. 뒤에서 보겠지만 이 비선형 한 겹이 없으면 층을 아무리 쌓아도 한 층과 똑같아진다 — 활성화 함수는 장식이 아니라 신경망을 신경망이게 하는 핵심 부품이다.
계단 함수 — 왜 이 몸으로는 배울 수 없나
계단 함수부터 넘파이로 만들어 눈으로 보자. 순진하게 짜면 스칼라 하나만 받지만, 신경망은 배열을 통째로 흘리므로 처음부터 배열을 받게 짠다.
import numpy as np
def step_function(x):
return np.array(x > 0, dtype=np.int64)
# x > 0 → 불리언 배열, dtype=int로 캐스팅하면 True→1, False→0
# 1장의 불리언 인덱싱과 같은 벡터화 기법이다. 루프가 없다.
x = np.array([-1.0, 1.0, 2.0])
print(step_function(x)) # [0 1 1]x > 0이 [False, True, True]라는 불리언 배열을 만들고, 그것을 정수로 캐스팅하면 [0, 1, 1]이 된다. for 루프 없이 배열 전체가 한 번에 처리되는 것 — 1장에서 익힌 벡터화 그대로다.
이제 이 함수의 문제를 그림으로 보자. -5부터 5까지 그려 보면 계단 함수는 0에서 수직으로 튀고, 그 양옆은 완전히 수평이다. 여기서 4장의 학습을 미리 상상해 보면 왜 이 몸이 안 되는지가 분명해진다. 학습은 “가중치를 조금 바꾸면 출력이 얼마나 바뀌나”라는 기울기를 보고 방향을 정한다. 그런데 계단 함수는 거의 모든 곳에서 기울기가 정확히 0이다(수평이니까). 가중치를 아무리 흔들어도 출력이 꿈쩍하지 않으니, “어느 방향으로 가야 나아지는지”라는 신호가 나오지 않는다. 유일하게 기울기가 있는 임계점에서는 기울기가 무한대로 발산한다. 어느 쪽이든 학습에 쓸 수 없다.
판단 기준: 활성화 함수를 고를 때 “이 함수는 입력이 조금 변할 때 출력이 매끄럽게 따라 변하는가”를 물어라 — 학습이 미분을 쓰기 때문이다. 함정: 계단 함수가 “생물학적 뉴런의 발화(all-or-nothing)와 닮아서 옳다”고 여기는 것. 생물학적 직관과 별개로, 미분 가능성이 없으면 경사법으로 학습할 수 없다. 신경망이 계단을 버린 것은 아름다움이 아니라 학습 가능성 때문이다.
시그모이드 — 계단을 매끄럽게 편 곡선
계단 함수의 정신(작은 입력은 0쪽, 큰 입력은 1쪽으로 누른다)은 살리되, 그 급격한 절벽을 매끄러운 비탈로 바꾼 것이 시그모이드 함수다.
이 식에는 낯선 기호 (자연상수, 약 2.718)와 지수가 들어 있다. 가 뭔지 감이 안 잡히면 먼저 수학부록4를 보고 오면 좋다 — 거기서 “는 무슨 수를 넣어도 결과가 양수이고 입력이 커질수록 급히 커진다”는 두 성질만 챙겨 오면 이 절이 전부 풀린다.
이 식이 왜 계단을 닮으면서도 매끄러운지 극한으로 확인해 보자. 가 아주 커지면 이므로 이고, 가 아주 작아지면(음의 큰 값) 이므로 이다. 그리고 에서는 이라 로 정확히 가운데를 지난다. 즉 양 끝은 계단 함수처럼 0과 1로 눌리지만, 그 사이를 절벽이 아니라 완만한 S자 비탈로 잇는다. 이 “사이의 비탈”이 있기에 어느 점에서든 기울기를 잴 수 있고, 학습 신호가 흐른다.
def sigmoid(x):
return 1 / (1 + np.exp(-x))
# np.exp(-x)가 배열 전체에 원소별로 적용된다(1장의 벡터화).
# 그래서 x가 (N, 50) 배치여도 코드는 그대로 한 줄이다.
x = np.array([-1.0, 1.0, 2.0])
print(sigmoid(x)) # [0.26894142 0.73105858 0.88079708]넘파이 덕에 이 한 줄이 스칼라든 (N, 50) 배치든 shape를 가리지 않고 원소마다 적용된다. 계단 함수와 겹쳐 그려 보면 둘의 관계가 한눈에 들어온다 — 시그모이드는 계단을 뭉갠 곡선이고, 계단은 시그모이드를 극단으로 밀어붙인 극한이다. 둘 다 출력을 사이로 누른다는 성질은 공유하되, 시그모이드만이 그 사이를 매끄럽게 연결한다.
그리고 결정적으로, 시그모이드는 어디서도 평평하지 않다(양 끝에서 기울기가 0에 가까워지긴 하지만 정확히 0은 아니다). 그래서 “가중치를 조금 바꾸면 출력이 조금 바뀐다”가 성립하고, 이 미세한 반응이 4장의 학습 신호가 된다. 시그모이드의 미분이 라는 깔끔한 형태로 떨어진다는 사실은 5장 역전파에서 다시 만난다 — 지금은 “매끄럽다”는 것만 챙겨 두자.
판단 기준: “매끄럽다”를 감이 아니라 극한으로 확인하라 — 큰 양수에서 1, 큰 음수에서 0, 0에서 0.5. 이 세 점만 짚으면 시그모이드의 모양이 손에 잡힌다. 함정: 시그모이드의 출력 범위 을 “확률”과 곧장 동일시하는 것. 은닉층의 시그모이드 출력은 확률이 아니라 그냥 눌린 신호값이다 — 확률은 출력층의 소프트맥스가 따로 맡는다(뒤에서 다룬다).
ReLU — 요즘 신경망이 더 자주 쓰는 활성화 함수
시그모이드가 오래 쓰였지만, 최근 신경망은 은닉층에 **ReLU(Rectified Linear Unit)**를 더 많이 쓴다. 정의는 놀랄 만큼 단순하다.
입력이 0보다 크면 그대로 흘려보내고, 0 이하면 0으로 막는다. 그게 전부다.
def relu(x):
return np.maximum(0, x)
# np.maximum은 원소별로 두 값 중 큰 쪽을 고른다(브로드캐스트로 0이 퍼진다).
# x가 (N, 50)이면 0이 (N, 50)로 브로드캐스트되어 원소마다 max(0, ·).
x = np.array([-2.0, -0.5, 3.0])
print(relu(x)) # [0. 0. 3.]np.maximum(0, x)에서 스칼라 0이 x의 shape로 브로드캐스트되어(1장의 그 규칙), 원소마다 max(0, 원소)가 계산된다. 여기서도 루프가 없다. ReLU가 시그모이드보다 선호되는 실질적 이유는 5장(역전파)과 6장(기울기 소실)에서 온전히 드러나지만, 미리 감만 잡자면 — 시그모이드는 양 끝에서 기울기가 0에 가까워져 깊은 망에서 학습 신호가 사그라드는 반면, ReLU는 양수 구간에서 기울기가 항상 1이라 신호가 죽지 않고 멀리까지 전달된다. 계산도 max 하나라 훨씬 가볍다.
판단 기준: 은닉층 기본값으로는 ReLU를 먼저 떠올려라 — 계산이 싸고 깊은 망에서 기울기가 잘 흐른다. 시그모이드는 출력을 로 눌러야 하는 특별한 자리(이진 확률 출력 등)에 남겨 둔다. 함정: ReLU가 음수 입력을 통째로 0으로 죽인다는 것 — 어떤 뉴런이 계속 음수만 받으면 영영 0만 내보내며 학습에서 배제될 수 있다(죽은 ReLU). 지금은 “0 이하를 자른다”는 성질만, 이 함정의 실체는 6장에서.
비선형이어야 하는 이유 — 선형만 쌓으면 한 층과 같다
여기서 이 장에서 가장 중요한 질문에 답해야 한다. 왜 활성화 함수는 반드시 비선형이어야 하는가? 계단이든 시그모이드든 ReLU든, 전부 직선이 아니라는 공통점이 있다. 이게 우연이 아님을, 반대로 활성화 함수가 선형이면 무슨 일이 벌어지는지 직접 계산해 증명해 보자.
활성화 함수가 선형 (는 상수)라고 하자. 그리고 3층을 쌓는다. 각 층은 활성화 함수를 통과하므로:
세 층을 통과했는데 결과는 그냥 , 즉 입력에 상수 하나()를 곱한 것이다. 이건 인 한 층짜리 선형 변환 와 완전히 똑같다. 가중치가 붙은 일반적인 경우로 넓혀도 결론은 같다 — 선형 변환 는 결합법칙으로 가 되고, 이라는 하나의 행렬로 대체된다. 층을 아무리 쌓아도 어떤 단일 행렬 하나로 접혀 버린다.
즉 선형 활성화로는 층을 쌓는 의미가 사라진다. 100층을 쌓아도 표현할 수 있는 것은 1층 선형 모델과 정확히 같은 집합 — 직선(초평면)뿐이다. 2장에서 단층 퍼셉트론이 XOR를 못 푼 것이 바로 이 한계였고, 층을 쌓아 그것을 넘어서려면 층과 층 사이에 반드시 비선형 한 겹이 끼어야 한다. 활성화 함수가 비선형인 이유가 이것이다 — 비선형성이 있어야 층을 쌓은 만큼 표현력이 실제로 늘어난다. 활성화 함수는 “층을 진짜 여러 층이게 만드는” 부품이다.
판단 기준: 새 활성화 함수를 볼 때 “이게 직선이 아닌가”를 먼저 확인하라 — 직선이면 층을 쌓는 이득이 0이다. 함정: 은닉층에 활성화 함수를 깜빡 빼먹거나(그러면 그 구간이 선형이 되어 층이 접힌다), 항등 함수 같은 선형을 은닉층에 쓰는 것. 문법상 여러 층이어도 수학적으로는 한 층이 되어, 깊게 만든 대가만 치르고 표현력은 못 얻는다.
순전파를 행렬 곱으로 — shape로 설계하기
이제 부품(활성화 함수)을 손에 쥐었으니 신경망 한 채를 조립한다. 신경망의 한 층이 하는 일은 1장에서 예고한 그대로다 — 입력에 가중치 행렬을 곱하고(가중합), 편향을 더하고, 활성화 함수를 통과시킨다. 그리고 이 모든 것을 샘플 하나가 아니라 여러 샘플을 한 번에 처리하도록 배치 형태로 짠다.
핵심은 shape 설계다. 행렬 곱이 아직 낯설면 수학부록3을 먼저 보고 오자 — 거기서 손으로 격자 두 개를 곱해 보면 여기서 쓰는 규칙이 손에 익는다. 그 부록에서 못 박은 행렬 곱 규칙 — 가운데 축이 맞물려 사라진다 — 이 하나로 모든 층의 차원이 결정된다. 입력이 2차원, 은닉층이 3뉴런·2뉴런, 출력이 2차원인 신경망을 예로 shape를 먼저 그려 보자.
각 화살표가 층 하나다. 가중치 행렬의 shape가 (그 층의 입력 뉴런 수, 그 층의 출력 뉴런 수)라는 규칙만 잡으면, 곱의 가운데 축이 자동으로 맞물려 사라지고 차원이 흘러간다. 편향은 각 층의 출력 뉴런 수만큼 있고, (뉴런 수,) 벡터가 배치의 모든 샘플에 브로드캐스트로 더해진다(1장의 편향 더하기 패턴 그대로). 코드를 짜기 전에 이 shape 표를 종이에 그리는 것이 순전파 구현의 절반이다.
판단 기준: 순전파 코드를 짜기 전에 각 배열의 shape를 화살표로 이어 그려, 곱의 가운데 축이 실제로 맞물리는지 손으로 확인하라 — 이게 맞으면 코드는 그 표의 번역일 뿐이다. 함정: 가중치를 (출력, 입력)으로 만들어 놓고 np.dot(X, W)를 하는 것. 그러면 가운데 축이 안 맞아 에러가 나거나(다행), 우연히 정사각이면 조용히 틀린 값이 나온다. W의 shape는 항상 (입력 뉴런 수, 출력 뉴런 수)로 고정하라.
3층 신경망 순전파를 계층별로 조립하기
이제 위 shape 설계를 코드로 자라나게 한다. 한 층씩 쌓으면서 각 단계의 shape를 주석으로 따라간다. 마지막에는 반복되는 “가중합 → 활성화”를 함수로 묶어 신경망 한 채를 완성한다.
import numpy as npdef sigmoid(x): return 1 / (1 + np.exp(-x))# 배치 입력: 샘플 2개, 각 특성 2개X = np.array([[1.0, 0.5], [2.0, 3.0]]) # X: (2, 2) (N=2, 특성=2)W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]]) # W1: (2, 3) 입력 2 → 은닉 3b1 = np.array([0.1, 0.2, 0.3]) # b1: (3,) 은닉 뉴런마다 하나A1 = np.dot(X, W1) + b1 # (2,2)·(2,3)=(2,3), +b1 브로드캐스트 → A1: (2, 3)Z1 = sigmoid(A1) # 원소별 활성화 → Z1: (2, 3)print(A1.shape, Z1.shape) # (2, 3) (2, 3)# 가운데 축(2)이 맞물려 사라지고 (N, 3)이 남았다. 이게 1장의 (a,b)·(b,c)→(a,c)다.
마지막 스텝이 신경망 추론의 뼈대 전부다. forward는 세 번의 가중합 → 활성화를 이어붙인 것에 지나지 않고, 각 줄의 shape 주석을 따라가면 입력 (N, 2)가 (N, 3) → (N, 2) → (N, 2)로 흐르는 게 보인다. 학습된 가중치만 갈아 끼우면 이 함수가 손글씨도 알아본다. 남은 것은 출력층을 문제 유형에 맞게 고르는 일이다.
판단 기준: 순전파를 “층마다 다른 특별한 코드”가 아니라 “같은 두 줄의 반복”으로 짜라 — 그래야 층을 늘리거나 줄일 때 구조가 흔들리지 않는다. 함정: 은닉층과 출력층에 같은 활성화 함수를 무심코 쓰는 것. 은닉층은 비선형(sigmoid/ReLU)이 필수지만, 출력층은 문제에 따라 항등이나 소프트맥스를 써야 한다 — 다음 절의 주제다.
출력층 — 회귀는 항등, 분류는 소프트맥스
출력층은 은닉층과 역할이 다르다. 은닉층의 활성화는 “신호를 비선형으로 비틀어 표현력을 만드는” 것이지만, 출력층의 활성화는 “신경망의 최종 답을 문제에 맞는 형태로 내놓는” 것이다. 그래서 문제 유형에 따라 다른 함수를 쓴다.
회귀(수치를 예측) — 예컨대 사진을 보고 나이를 맞히는 문제라면, 출력이 그냥 실수 하나면 된다. 이때는 항등 함수를 써서 가중합을 그대로 내보낸다. 위 스텝에서 쓴 identity가 그것이다.
분류(범주를 예측) — 손글씨가 0~9 중 무엇인지 맞히는 문제라면, 출력이 “각 클래스일 확률”이면 좋다. 이때 쓰는 것이 소프트맥스 함수다.
분모의 기호는 “를 1부터 까지 바꿔 가며 다 더하라”는 합 기호다. 이 시그마 표기가 낯설면 수학부록1을 먼저 훑고 오면 좋다. 그리고 왜 하필 지수 를 쓰고 이걸 확률로 읽을 수 있는지는 수학부록4에서 숫자로 떼어 두었다 — 여기서는 그 결론만 이어 받아 유도한다.
번째 출력 는 번째 입력 의 지수를, 모든 입력의 지수 합으로 나눈 값이다. 이 식이 왜 “확률”에 어울리는지 두 성질로 확인하자. 첫째, 는 항상 양수이므로 모든 이다. 둘째, 분모가 모든 항의 합이므로 전부 더하면 이다. 각 출력이 0과 1 사이이고 총합이 1 — 확률 분포의 정의 그대로다. 그래서 소프트맥스의 출력은 “이 입력이 각 클래스일 확률”로 해석할 수 있다. 지수를 쓰는 덕에 가 조금만 커도 그 클래스의 확률이 크게 벌어져(가장 큰 것을 부드럽게 강조), “soft”한 max가 된다.
한 가지 실용적 사실 — 추론(어느 클래스인지 고르기)만 할 거라면 소프트맥스를 생략해도 답은 같다. 소프트맥스는 단조 증가라서 가 가장 큰 클래스가 곧 가 가장 큰 클래스이기 때문이다. 그래서 실제 추론 코드에서는 출력층 소프트맥스를 자주 건너뛴다. 다만 소프트맥스는 4장의 학습(손실 계산)에서 반드시 필요하므로, 지금 그 유도를 확실히 해 둔다.
판단 기준: 출력층 함수는 “무엇을 예측하나”로 정하라 — 수치면 항등, 범주면 소프트맥스. 그리고 추론만 하면 소프트맥스는 생략 가능(argmax는 그대로), 학습하면 필수(손실이 확률을 요구)임을 구분하라. 함정: 회귀 문제에 소프트맥스를 씌우거나, 분류 문제의 출력을 확률로 정규화하지 않고 손실을 재는 것. 문제 유형과 출력층·손실은 한 세트로 움직인다(4장에서 소프트맥스와 교차 엔트로피가 짝을 이루는 이유가 여기서 시작된다).
소프트맥스의 오버플로 — 최댓값 빼기 유도
소프트맥스를 식 그대로 코드로 옮기면 큰 함정에 빠진다. 지수 함수 는 폭발적으로 커진다 — 은 컴퓨터가 표현할 수 있는 범위를 아득히 넘어 inf(무한대)가 되고, inf / inf는 nan(수 아님)이 된다. 신경망의 가중합 는 학습 중 얼마든지 커질 수 있으므로, 이 오버플로는 이론이 아니라 실제로 터지는 버그다.
a = np.array([1010, 1000, 990])
print(np.exp(a) / np.sum(np.exp(a)))
# [nan nan nan] ← e^1010이 inf가 되어 계산이 무너졌다해법은 식을 변형하는 데 있다. 소프트맥스는 분자와 분모에 같은 상수 를 곱해도 값이 변하지 않는다 — 그 성질을 이용해 지수의 크기를 강제로 낮춘다. 유도해 보자. 임의의 상수 를 분자·분모에 곱하고, 그것을 지수 안으로 밀어 넣는다(이고 이므로):
즉 모든 입력에 같은 상수 를 더해도 소프트맥스 값은 그대로다. 그렇다면 지수가 폭발하지 않도록 를 고르면 된다. 가장 자연스러운 선택은 입력의 최댓값을 빼는 것()이다. 그러면 가장 큰 항이 이 되고 나머지는 전부 그보다 작으니, 지수가 결코 inf로 넘치지 않는다.
값은 원래 소프트맥스와 정확히 같으면서, 오버플로만 사라졌다. 이 “최댓값 빼기”를 코드로 자라나게 해 보자.
import numpy as npdef softmax_naive(a): exp_a = np.exp(a) # a가 크면 여기서 inf가 튄다 return exp_a / np.sum(exp_a) # inf / inf → nana = np.array([0.3, 2.9, 4.0])print(softmax_naive(a)) # [0.01821127 0.24519181 0.73659691] 작은 값이면 OKbig = np.array([1010.0, 1000.0, 990.0])print(softmax_naive(big)) # [nan nan nan] 큰 값이면 무너진다
세 번째 스텝의 keepdims=True가 1장에서 다룬 브로드캐스트 함정의 정확한 사례다. np.max(A, axis=1)은 (N,)을 돌려주는데, 이걸 (N, C)에서 빼려 하면 안쪽 축이 안 맞아 실패하거나 엉뚱하게 브로드캐스트된다. keepdims=True로 (N, 1)을 유지해야 “각 행에서 그 행의 최댓값을 뺀다”가 올바르게 성립한다. shape를 축까지 신경 쓰는 습관이 여기서 값을 한다.
판단 기준: 지수·로그가 들어간 수식을 코드로 옮길 때는 “입력이 커지면 넘치지 않나”를 반드시 점검하고, 소프트맥스라면 항상 최댓값을 빼라 — 값은 그대로, 안정성만 얻는다. 함정: 배치 소프트맥스에서 axis나 keepdims를 빠뜨리는 것. axis 없이 전체 최댓값·합을 쓰면 샘플들이 섞여 틀린 확률이 나오고, keepdims 없이 (N,)으로 두면 브로드캐스트가 어긋난다. 축과 차원 유지를 함께 챙겨야 한다.
숫자로 따라가기 — 2→3→2 신경망을 손으로 굴려 보기
지금까지 부품을 다 만들었으니, 아주 작은 신경망 하나를 구체적인 숫자로 처음부터 끝까지 손으로 굴려 보자. 추상 수식은 손에서 자꾸 빠져나가지만, 종이에 숫자를 적어 가며 한 칸씩 채우면 순전파가 “그저 곱하고 더하고 눌러 담기의 반복”임이 몸에 남는다. 입력 2, 은닉층 3(시그모이드), 출력층 2(소프트맥스)인 2→3→2 신경망을 쓴다.
입력과 가중치를 다음으로 정한다(샘플 하나, shape (2,)).
1단계 — 은닉층 가중합 . shape는 . 수학부록3에서 익힌 대로, 결과의 번째 칸은 “입력 벡터 × 의 열, 짝지어 곱해 더하고 편향을 얹은 것”이다.
2단계 — 시그모이드로 눌러 담기 . 원소마다 를 씌운다. shape는 그대로 이다(가 뭔지 흐릿하면 수학부록4).
세 값 모두 이 양수라 시그모이드가 0.5보다 크게 눌렀다. 입력이 클수록() 출력도 1에 더 가깝다(). 계단 함수라면 셋 다 그냥 1이 됐겠지만, 시그모이드는 그 사이의 미묘한 차이를 살려 둔다 — 이 살아남은 차이가 나중에 학습 신호가 된다.
3단계 — 출력층 가중합 . 이제 은닉층 출력 이 다음 층의 입력이 된다. shape는 . 의 앞 축(3)이 의 축(3)과 맞물려 사라진다.
4단계 — 소프트맥스로 확률 만들기 . 분류 문제이므로 출력층은 소프트맥스다. 먼저 각 항에 지수를 씌운다.
분모는 이 둘의 합이다.
각 항을 분모로 나눈다.
두 값을 더하면 , 확률의 규칙 그대로다. 이 신경망은 이 입력을 “약 66.8% 확신으로 2번 클래스”라고 본다. argmax를 씌우면 인덱스 1(둘째 클래스)이 답이다. 입력 (2,) 하나가 를 거쳐 확률 (2,)로 나오기까지, 우리가 한 일은 오직 곱하고·더하고·눌러 담고·다시 곱하고·지수 씌워 정규화한 것뿐이다.
같은 계산에서 오버플로가 터지는 경우. 위 출력층 가중합이 만약 처럼 큰 값이었다면(학습 중 가중치가 커지면 실제로 이런 일이 생긴다), 나이브 소프트맥스는 무너진다.
이때 최댓값 을 모든 항에서 빼면, 지수의 입력이 원래의 작은 값으로 돌아온다.
결과 는 최댓값을 빼기 전과 소수점까지 정확히 같다. 최댓값 빼기는 값을 바꾸지 않고 오버플로만 없앤다는 앞 절의 유도가, 이렇게 같은 숫자로 손안에서 확인된다. 큰 값 버전에서 nan이 나던 것이 작은 값 버전과 똑같은 정답으로 돌아왔다.
판단 기준: 순전파가 헷갈리면 이렇게 작은 망을 골라 종이에 직접 굴려 보라 — 각 단계의 값과 shape를 손으로 적으면 “어디서 축이 맞물리고 어디서 확률이 되는지”가 코드보다 먼저 손에 잡힌다. 함정: 손계산에서 지수·나눗셈 반올림을 너무 거칠게 하는 것. 을 0.74로 뭉개고 넘어가면 뒤 단계에서 오차가 눈덩이처럼 불어난다. 중간값은 소수 넷째 자리쯤까지 남겨 두고, 최종 확률의 합이 1이 되는지로 검산하라.
MNIST 손글씨 추론 — 배운 가중치로 답을 내다
이제 부품이 다 모였다. 실제 데이터로 순전파를 돌려 본다. MNIST는 손으로 쓴 숫자 0~9의 흑백 이미지 데이터셋으로, 각 이미지는 28×28 픽셀이다. 이 책은 학습된 가중치(sample_weight.pkl)를 제공하므로, 4장의 학습을 기다리지 않고도 “학습된 신경망이 어떻게 추론하는지”를 지금 확인할 수 있다.
핵심은 shape의 흐름이다. 28×28 이미지를 한 줄로 펴면 (784,) 벡터가 되고(1장에서 이미지가 곧 배열이라 했던 그 감각), 이것이 입력층 784뉴런에 들어간다. 은닉층은 50뉴런, 100뉴런, 출력층은 0~9에 대응하는 10뉴런이다. 순전파는 앞서 만든 forward와 똑같은 “가중합 → 활성화”의 반복이고, 출력층만 소프트맥스다 — 방금 손으로 굴린 2→3→2 계산과 구조가 완전히 같고, 뉴런 수만 커졌을 뿐이다.
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def softmax(a):
c = np.max(a)
exp_a = np.exp(a - c)
return exp_a / np.sum(exp_a)
def predict(network, x):
W1, W2, W3 = network["W1"], network["W2"], network["W3"]
b1, b2, b3 = network["b1"], network["b2"], network["b3"]
# x: (784,) 이미지 한 장을 편 것
z1 = sigmoid(np.dot(x, W1) + b1) # (784,)·(784,50)=(50,) → z1: (50,)
z2 = sigmoid(np.dot(z1, W2) + b2) # (50,)·(50,100)=(100,) → z2: (100,)
y = softmax(np.dot(z2, W3) + b3) # (100,)·(100,10)=(10,) → y: (10,)
return y # 각 숫자(0~9)일 확률 10개
# x_test: (10000, 784), t_test: (10000,) 라고 하자(정답 레이블)
accuracy = 0
for i in range(len(x_test)):
y = predict(network, x_test[i]) # y: (10,)
p = np.argmax(y) # 확률이 가장 큰 클래스의 인덱스 = 예측 숫자
if p == t_test[i]:
accuracy += 1
print(accuracy / len(x_test)) # 0.9352 약 93.5% 정답np.argmax(y)가 확률 10개 중 가장 큰 것의 인덱스를 돌려주고, 그게 곧 신경망이 고른 숫자다(앞서 말한 대로 추론에서는 소프트맥스를 빼도 argmax 결과는 같지만, 여기선 확률을 보여 주기 위해 남겼다). 학습을 한 줄도 안 했는데 약 93.5%를 맞힌다 — 물론 이 정확도는 우리가 만든 게 아니라 빌려 온 가중치의 것이다. 4장부터 저 가중치를 우리 손으로 학습해 채운다.
판단 기준: 추론 파이프라인을 짤 때 이미지의 원래 형상 (28,28)과 신경망 입력 (784,), 출력 (10,)을 명확히 구분해 흐름을 그려라 — 어디서 펴고 어디서 확률이 나오는지가 shape에 다 적혀 있다. 함정: 픽셀값 정규화(0255를 01로)를 빠뜨리거나 이미지를 안 펴고 넣는 것. 학습 때와 다른 전처리를 추론에 쓰면 shape는 맞아도 정확도가 곤두박질친다 — 전처리는 학습·추론이 반드시 같아야 한다.
배치 처리 — 한 장씩 말고 묶어서
위 코드는 이미지를 한 장씩 predict에 넣어 만 번 반복한다. 이걸 여러 장 묶어 한 번에 처리하면 훨씬 빠르다. 이것을 배치(batch) 처리라 한다. 왜 빠른가? 넘파이의 행렬 곱은 내부적으로 고도로 최적화된 수치 라이브러리를 부르는데, 큰 행렬 하나를 곱하는 것이 작은 행렬을 여러 번 곱하는 것보다 훨씬 효율적이기 때문이다. 파이썬 for 루프의 오버헤드도 줄어든다.
shape만 한 축(배치 축) 늘리면 끝이다. 앞서 순전파를 처음부터 (N, ...) 배치 형태로 설계해 둔 것이 여기서 값을 한다 — 코드를 거의 안 고쳐도 된다.
batch_size = 100
accuracy = 0
for i in range(0, len(x_test), batch_size):
x_batch = x_test[i:i+batch_size] # x_batch: (100, 784) 100장 묶음
y_batch = predict_batch(network, x_batch) # y_batch: (100, 10)
p = np.argmax(y_batch, axis=1) # axis=1: 각 행(샘플)마다 최댓값 인덱스 → (100,)
accuracy += np.sum(p == t_test[i:i+batch_size]) # 맞은 개수 누적
print(accuracy / len(x_test)) # 0.9352 한 장씩과 정확히 같은 정답률predict_batch는 앞의 predict에서 softmax만 배치판(축 지정)으로 바꾼 것으로, 내부의 np.dot은 (100,784)·(784,50)=(100,50)처럼 배치 축을 그대로 통과시킨다. 결과 정확도는 한 장씩과 완전히 같다 — 배치는 계산을 빠르게 할 뿐 답을 바꾸지 않는다. 여기서 주목할 것은 np.argmax(y_batch, axis=1)이다. axis=1을 줘야 각 행(각 샘플)에서 최댓값 위치를 찾는다. axis를 빼면 배열 전체에서 딱 하나의 최댓값을 찾아 완전히 틀린 답이 나온다 — 소프트맥스 배치판에서 본 그 축 함정과 같은 뿌리다.
판단 기준: 반복 추론이 느리면 루프를 배치 행렬 곱으로 바꿀 수 있는지 먼저 보라 — 순전파를 (N, ...)로 설계해 뒀다면 슬라이싱과 축 지정만으로 끝난다. 함정: 배치로 바꾸면서 argmax·sum·max의 axis를 빠뜨리는 것. 한 장일 때는 축이 없어도 우연히 맞지만, 배치가 되는 순간 샘플들이 뒤섞여 틀린다. 배치 처리의 버그는 거의 다 “축을 안 줘서”다.
정리 — 추론하는 신경망을 밑바닥에서 세웠다
이 장은 학습을 한 줄도 하지 않았지만, 학습된 신경망이 어떻게 답을 내는지를 밑바닥까지 분해했다. 다음 장은 여기에 “가중치를 스스로 정하는” 학습을 붙인다.
- 퍼셉트론과 신경망의 차이는 구조가 아니라 활성화 함수다. 계단 함수(미분이 0/무한대라 학습 불가)를 매끄러운 시그모이드·ReLU로 바꾼 것이 신경망으로 가는 문이다.
- 활성화 함수는 반드시 비선형이어야 한다. 선형만 쌓으면 처럼 하나의 층으로 접혀, 층을 쌓은 이득이 사라진다. 비선형성이 표현력을 만든다.
- 순전파는 “가중합() → 활성화”의 반복이고, 모든 차원은 행렬 곱 규칙 로 결정된다. 가중치 shape는 항상
(입력 뉴런 수, 출력 뉴런 수). - 출력층은 문제로 정한다 — 회귀는 항등, 분류는 소프트맥스 (출력이 확률: 양수이고 합이 1). 지수가 넘치므로 최댓값을 빼서 계산하되 값은 동일하게 유지한다.
- 작은 2→3→2 신경망을 실제 숫자로 손계산하면, 순전파는 처럼 곱·덧셈·눌러 담기의 반복일 뿐이고, 최댓값을 빼도 소프트맥스 결과가 그대로임을 소수점까지 확인할 수 있다.
- 학습된 가중치를 빌려 순전파만으로 MNIST를 약 93.5% 맞혔다. 이미지
(28,28)→ 입력(784,)→ 출력(10,)의 shape 흐름과argmax로 답을 고르는 과정이 추론의 전부다. - 배치 처리는 shape에 배치 축 하나를 더해 한 번에 여러 장을 곱하는 것으로, 답은 그대로면서 빨라진다.
axis지정만 빠뜨리지 않으면 된다.
다음 장은 이 신경망에 눈을 뜨게 한다 — 데이터를 주면 가중치를 스스로 정하는 “학습”. 손으로 채웠던 저 가중치들을 손실 함수와 경사하강법으로 우리 손으로 구해 나간다.
다음장으로 4장