Neural Network 개념: 활성함수가 선형 층을 비선형 학습기로 바꾼다
신경망을 뉴런, 선형 층, 활성함수, 다층 퍼셉트론 순서로 조립해 이해한다. NumPy로 2층 다층 퍼셉트론의 순전파를 구현하고, 입력부터 출력까지 배열 모양이 어떻게 변하는지 추적한다. 활성함수를 제거하면 여러 선형 층도 결국 하나의 선형 변환과 같아진다는 사실을 실험한다.
By 송민성
오늘의 목표
신경망이 입력 숫자를 받아 여러 단계의 계산을 거쳐 출력으로 바꾸는 과정을 그림처럼 이해한다. NumPy로 2층 다층 퍼셉트론의 순전파를 직접 구현하고 각 단계의 배열 모양을 설명할 수 있게 한다.
개념과 직관
1. 뉴런: 입력을 섞고 판단하는 작은 계산 단위
뉴런(neuron, 여러 입력을 가중합한 뒤 활성함수를 적용하는 계산 단위)은 다음 두 단계를 수행한다.
- 입력마다 중요도를 곱해 모두 더한다.
- 계산 결과를 활성함수에 통과시킨다.
입력이 x1, x2이고 가중치가 w1, w2, 편향이 b라면 계산은 다음과 같다.
z = x1 × w1 + x2 × w2 + b
출력 = 활성함수(z)
가중치(weight, 입력이 결과에 미치는 영향의 크기)는 각 입력의 중요도를 나타낸다. 편향(bias, 입력과 무관하게 결과를 이동시키는 값)은 판단 기준선을 옮긴다.
예를 들어 과일의 무게와 색 농도로 사과인지 판단한다고 하자.
무게 ──× 가중치 ──┐
├── 모두 더하기 ── 활성함수 ── 출력
색 농도 ─× 가중치 ─┘ ↑
편향처음에는 가중치와 편향을 무작위로 정할 수 있다. 학습은 좋은 출력을 만들도록 이 값들을 조정하는 과정이다. 오늘은 조정 과정이 아니라, 주어진 값으로 출력을 계산하는 순전파(forward pass, 입력에서 출력 방향으로 계산하는 과정)만 다룬다.
2. 선형 층: 여러 뉴런을 한 줄로 배치하기
뉴런 하나는 값 하나를 출력한다. 뉴런 여러 개를 나란히 놓으면 여러 값을 동시에 출력할 수 있다. 이것이 선형 층(linear layer, 입력 벡터에 가중치 행렬을 곱하고 편향을 더하는 층)이다.
입력 샘플이 3개 특징을 가지고 있고 뉴런이 4개라면 다음처럼 변한다.
입력 3개 값 출력 4개 값
[x1, x2, x3] ── 선형 층 ──> [z1, z2, z3, z4]
뉴런 4개여러 샘플을 한꺼번에 처리할 때 수식은 다음과 같다.
Z = XW + b
각 배열의 모양은 다음과 같다.
X: (샘플 수, 입력 특징 수)
W: (입력 특징 수, 출력 특징 수)
b: (출력 특징 수,)
Z: (샘플 수, 출력 특징 수)예를 들어 샘플 2개, 입력 특징 3개, 출력 특징 4개라면 다음과 같다.
X: (2, 3)
W: (3, 4)
b: (4,)
Z: (2, 4)행렬 곱에서는 가운데 크기가 같아야 한다. (2, 3) @ (3, 4)의 결과는 바깥 크기인 (2, 4)가 된다.
3. 활성함수: 직선만 그리는 한계를 깨기
활성함수(activation function, 선형 계산 결과를 비선형으로 바꾸는 함수)는 신경망이 단순한 직선보다 복잡한 규칙을 표현하게 만든다.
ReLU
ReLU(rectified linear unit, 음수는 0으로 바꾸고 양수는 유지하는 함수)는 다음처럼 계산한다.
ReLU(z) = z가 0보다 크면 z, 아니면 0
입력: [-2, -0.5, 0, 1, 3]
출력: [ 0, 0, 0, 1, 3]그래프로 보면 음수 구간은 바닥에 붙고, 양수 구간은 대각선으로 올라간다.
출력
|
| /
| /
|______/
+------------- 입력시그모이드
시그모이드(sigmoid, 모든 실수를 0과 1 사이 값으로 압축하는 함수)는 다음처럼 계산한다.
sigmoid(z) = 1 / (1 + exp(-z))
큰 양수는 1에 가까워지고, 큰 음수는 0에 가까워진다. 출력값을 0과 1 사이로 제한하고 싶을 때 사용할 수 있다.
입력: 매우 작은 음수 0 매우 큰 양수
출력: 0에 가까움 0.5 1에 가까움4. 활성함수 없이 층만 쌓으면 왜 부족한가
선형 층 두 개를 연속으로 계산해 보자.
H = XW1 + b1
Y = HW2 + b2
첫 번째 식을 두 번째 식에 넣으면 다음과 같다.
Y = (XW1 + b1)W2 + b2
정리하면 다음과 같다.
Y = X(W1W2) + (b1W2 + b2)
W1W2를 새로운 가중치 W, (b1W2 + b2)를 새로운 편향 b라고 부르면 다음 식이 된다.
Y = XW + b
선형 층을 두 개 쌓았지만 하나의 선형 층과 같은 형태다. 열 개를 쌓아도 활성함수가 없다면 결과는 여전히 하나의 선형 변환으로 합칠 수 있다.
활성함수를 사이에 넣으면 계산이 달라진다.
H = ReLU(XW1 + b1)
Y = HW2 + b2
ReLU가 음수를 0으로 꺾기 때문에 두 행렬 곱을 하나로 합칠 수 없다. 층마다 입력 공간을 다른 방향으로 자르고 꺾으면서 복잡한 경계를 표현할 수 있다.
5. MLP: 선형 층과 활성함수를 쌓은 구조
다층 퍼셉트론(MLP, multilayer perceptron, 선형 층과 활성함수를 여러 단계로 연결한 신경망)은 보통 다음 구조를 가진다.
입력층 은닉층 출력층
3개 값 4개 값 2개 값
[x1] [h1] [y1]
[x2] ──> [h2] ── ReLU ──> [y2]
[x3] [h3]
[h4]은닉층(hidden layer, 입력과 최종 출력 사이에서 중간 표현을 만드는 층)은 입력 특징을 새로운 특징으로 바꾼다. 은닉층의 숫자는 사람이 의미를 직접 정한 값이 아니다. 가중치와 입력의 조합으로 만들어진 중간 계산 결과다.
오늘 구현할 2층 MLP는 선형 층이 두 개라는 뜻이다.
입력 X
↓
첫 번째 선형 층
↓
ReLU
↓
두 번째 선형 층
↓
출력 Y코드로 직접 해보기
1. 활성함수 만들기
NumPy는 숫자 배열과 행렬 계산을 제공하는 라이브러리다.
import numpy as np
def relu(x):
return np.maximum(0, x)
def sigmoid(x):
return 1 / (1 + np.exp(-x))
values = np.array([-2.0, 0.0, 2.0])
print("ReLU:", relu(values))
print("sigmoid:", sigmoid(values))예상 출력은 다음과 비슷하다.
ReLU: [0. 0. 2.]
sigmoid: [0.119... 0.5 0.880...]2. 입력과 가중치 준비하기
샘플 2개를 한 번에 처리한다. 각 샘플에는 특징 3개가 있다.
import numpy as np
np.random.seed(42)
X = np.array([
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
])
W1 = np.random.randn(3, 4)
b1 = np.random.randn(4)
W2 = np.random.randn(4, 2)
b2 = np.random.randn(2)
print("X :", X.shape)
print("W1:", W1.shape)
print("b1:", b1.shape)
print("W2:", W2.shape)
print("b2:", b2.shape)모양은 다음과 같다.
X : (2, 3)
W1: (3, 4)
b1: (4,)
W2: (4, 2)
b2: (2,)np.random.seed(42)는 난수 생성 순서를 고정한다. 코드를 다시 실행해도 같은 무작위 가중치가 만들어져 결과를 비교하기 쉽다.
3. 2층 MLP 순전파 구현하기
import numpy as np
np.random.seed(42)
X = np.array([
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
])
W1 = np.random.randn(3, 4)
b1 = np.random.randn(4)
W2 = np.random.randn(4, 2)
b2 = np.random.randn(2)
Z1 = X @ W1 + b1
H = np.maximum(0, Z1)
Y = H @ W2 + b2
print("입력 X: ", X.shape)
print("첫 선형 Z1: ", Z1.shape)
print("은닉 표현 H: ", H.shape)
print("출력 Y: ", Y.shape)
print("\n출력값:")
print(Y)모양의 흐름은 다음과 같다.
X : (2, 3)
Z1 : (2, 4)
H : (2, 4)
Y : (2, 2)ReLU는 숫자만 바꾸고 배열 모양은 바꾸지 않는다. 모양을 바꾸는 주체는 선형 층의 가중치 행렬이다.
b1은 (4,)인데 (2, 4)인 X @ W1에 더할 수 있다. NumPy가 같은 편향을 각 샘플 행에 반복해 더하기 때문이다. 이를 브로드캐스팅(broadcasting, 작은 배열을 알맞게 확장해 계산하는 규칙)이라고 한다.
4. 함수로 묶기
import numpy as np
def relu(x):
return np.maximum(0, x)
def mlp_forward(x, w1, b1, w2, b2):
hidden = relu(x @ w1 + b1)
output = hidden @ w2 + b2
return hidden, output
np.random.seed(42)
X = np.array([
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
])
W1 = np.random.randn(3, 4)
b1 = np.random.randn(4)
W2 = np.random.randn(4, 2)
b2 = np.random.randn(2)
hidden, output = mlp_forward(X, W1, b1, W2, b2)
print("은닉층 모양:", hidden.shape)
print("출력층 모양:", output.shape)
assert hidden.shape == (2, 4)
assert output.shape == (2, 2)assert는 조건이 맞는지 검사한다. 배열 모양이 예상과 다르면 즉시 오류가 발생하므로 계산 실수를 빨리 찾을 수 있다.
오늘의 실험
활성함수를 제거한 2층 신경망이 하나의 선형 층과 같은지 숫자로 확인한다.
import numpy as np
np.random.seed(42)
X = np.random.randn(5, 3)
W1 = np.random.randn(3, 4)
b1 = np.random.randn(4)
W2 = np.random.randn(4, 2)
b2 = np.random.randn(2)
# 선형 층 두 개를 순서대로 통과
two_layers = (X @ W1 + b1) @ W2 + b2
# 두 층을 하나의 가중치와 편향으로 합치기
combined_W = W1 @ W2
combined_b = b1 @ W2 + b2
one_layer = X @ combined_W + combined_b
difference = np.max(np.abs(two_layers - one_layer))
print("두 층 결과:")
print(two_layers)
print("\n합친 한 층 결과:")
print(one_layer)
print("\n가장 큰 차이:", difference)
assert np.allclose(two_layers, one_layer)np.allclose()는 두 배열이 작은 계산 오차를 제외하면 같은지 확인한다. 컴퓨터의 소수 계산에는 매우 작은 반올림 오차가 생길 수 있으므로 ==보다 적합하다.
다음으로 첫 번째 층 뒤에 ReLU를 넣는다.
with_relu = np.maximum(0, X @ W1 + b1) @ W2 + b2
print("활성함수 없는 결과와 같은가?:", np.allclose(with_relu, one_layer))
print("ReLU를 넣은 결과:")
print(with_relu)대부분 False가 출력된다. ReLU가 중간값 일부를 0으로 바꾸므로 기존 방식으로 두 층을 하나에 합칠 수 없다.
실험 결과를 다음 표로 기록한다.
| 실험 | 중간 활성함수 | 한 선형 층과 같은가 | 관찰 |
|---|---|---|---|
| 1 | 없음 | 예 | 최대 차이가 0에 매우 가까움 |
| 2 | ReLU | 아니요 | 음수 중간값이 0으로 바뀌며 출력도 달라짐 |
| 3 | sigmoid | 아니요 | 중간값이 0과 1 사이로 압축됨 |
추가로 바꿔볼 값은 세 가지다.
- 입력값
X를 다른 숫자로 바꾼다. - 은닉층 크기를 4에서 2 또는 8로 바꾼다.
- ReLU를 sigmoid로 교체하고 출력 범위를 관찰한다.
은닉층 크기를 바꾸면 W1의 두 번째 크기와 W2의 첫 번째 크기를 함께 바꿔야 한다.
왜 이걸 배우나 (LLM / Post-training 연결)
LLM도 큰 관점에서는 선형 변환, 활성함수, 여러 층의 반복으로 입력 표현을 계속 바꾸는 신경망이다. 이후 모델 구조를 읽을 때 가중치 행렬, 은닉 표현, 순전파, 배열 모양을 추적하는 능력이 계속 사용된다.
LLM 후속 학습(post-training, 사전 학습된 모델을 지시 수행이나 선호에 맞게 추가 학습하는 과정)에서는 모델 출력과 목표 사이의 차이를 줄이도록 수많은 가중치를 조정한다. 오늘은 그 출발점인 입력 → 중간 표현 → 출력 계산을 익힌다.
초보자가 막히는 지점
행렬 곱과 원소별 곱을 혼동한다
X @ W는 행렬 곱이다. X * W는 같은 위치의 숫자끼리 곱하는 원소별 곱이다. 선형 층에서는 @를 사용한다.
배열 모양이 맞지 않는다
다음 오류가 자주 나온다.
ValueError: matmul: Input operand ... has a mismatch앞 배열의 마지막 크기와 뒤 배열의 첫 번째 크기를 확인한다.
(2, 3) @ (3, 4) → 가능
(2, 3) @ (4, 3) → 불가능층의 개수와 뉴런 개수를 혼동한다
2층 MLP는 보통 학습 가능한 선형 층이 두 개라는 뜻이다. 첫 번째 층에 뉴런 4개가 있어도 4층 신경망은 아니다.
ReLU가 학습한다고 오해한다
ReLU에는 오늘 코드 기준으로 조정할 가중치가 없다. 들어온 음수를 0으로 바꾸는 고정 함수다. 학습되는 값은 주로 선형 층의 가중치와 편향이다.
무작위 출력에 의미를 부여한다
현재 가중치는 무작위라서 출력값 자체에는 유용한 의미가 없다. 오늘 확인할 대상은 예측 정확도가 아니라 계산 흐름과 배열 모양이다.
알고리즘 (병행 1시간)
Easy 1문제: LeetCode 1번 Two Sum
정수 배열과 목표값이 주어졌을 때 합이 목표값이 되는 두 원소의 인덱스를 찾는다.
- 먼저 이중 반복문으로 구현한다.
- 이후 딕셔너리에 이미 본 숫자와 인덱스를 저장하는 방식으로 개선한다.
- 시간복잡도를
O(n²)에서O(n)으로 줄인 이유를 설명한다.
Medium 1문제: LeetCode 54번 Spiral Matrix
2차원 배열을 바깥 테두리부터 시계 방향 나선 순서로 읽는다.
- 위쪽, 아래쪽, 왼쪽, 오른쪽 경계를 변수로 둔다.
- 한 방향을 읽을 때마다 해당 경계를 한 칸 줄인다.
- 행이나 열이 하나만 남은 경우 중복 방문하지 않도록 조건을 확인한다.
각 문제를 푼 뒤 다음 세 항목을 기록한다.
- 접근: 어떤 자료구조와 반복 순서를 선택했는가
- 시간복잡도: 입력 크기가 커질 때 연산 횟수가 어떻게 증가하는가
- 틀린 이유: 인덱스 범위, 중복 방문, 빠뜨린 예외 중 무엇이 원인이었는가