다층 네트워크와 순전파(Multi-Layer Networks and Forward Pass)

뉴런 하나는 직선을 그립니다. 그것을 쌓으면 어떤 형태든 그릴 수 있습니다.

유형: Build

언어: Python

선수 지식: Phase 01 수학 기초(Math Foundations), Lesson 03.01 퍼셉트론(The Perceptron)

소요 시간: 약 90분

학습 목표

  • 완전한 순전파(forward pass)를 수행하는 Layer 클래스(class)와 Network 클래스(class)로 다층 네트워크(multi-layer network)를 처음부터 만듭니다.
  • 네트워크의 각 층을 지나며 행렬 차원(matrix dimensions)을 추적하고 형태 불일치(shape mismatch)를 찾아냅니다.
  • 비선형 활성화(nonlinear activation) 함수를 쌓으면 네트워크가 곡선형 결정 경계(curved decision boundary)를 학습할 수 있는 이유를 설명합니다.
  • 손으로 조정한 시그모이드(sigmoid) 가중치(weights)를 사용해 2-2-1 구조(architecture)로 XOR 문제를 풉니다.

문제

단일 뉴런은 직선을 그리는 도구입니다. 그뿐입니다. 데이터 위에 직선 하나를 긋습니다. 하지만 이미지 인식, 언어 이해, 바둑 같은 실제 AI 문제는 곡선을 필요로 합니다. 뉴런을 층으로 쌓아야 곡선을 만들 수 있습니다.

1969년에 민스키와 페퍼트(Minsky and Papert)는 이 한계가 치명적이라는 것을 보였습니다. 단일층 네트워크(single-layer network)는 XOR을 학습할 수 없습니다. "학습하기 어렵다"가 아니라 수학적으로 불가능합니다. XOR 진리표(truth table)는 [0, 1], [1, 0]을 한쪽에 두고 [0, 0], [1, 1]을 다른 쪽에 둡니다. 이를 나누는 단일 직선은 없습니다.

이 사실은 10년 넘게 신경망 연구 지원을 위축시켰습니다. 되돌아보면 해결책은 단순합니다. 한 층만 쓰지 말고, 뉴런을 층으로 쌓으면 됩니다. 첫 번째 층이 입력 공간을 새로운 특성(feature)으로 조각내고, 두 번째 층이 그 특성을 조합해 단일 직선으로는 만들 수 없는 결정을 만듭니다.

이렇게 쌓아 올린 구조가 바로 다층 네트워크(multi-layer network)입니다. 오늘날 운영 환경(production)에서 쓰이는 모든 딥러닝(deep learning) 모델의 토대입니다. 입력에서 은닉층을 거쳐 출력으로 데이터가 흐르는 순전파는 다른 모든 것이 작동하기 전에 가장 먼저 만들어야 하는 구조입니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.다층 네트워크(multi-layer network)에서 은닉층(hidden layer)의 목적은 무엇인가요?

2.신경망에서 순전파(forward pass)는 무엇을 하나요?

0/2 답변 완료

개념

입력층, 은닉층, 출력층

다층 네트워크에는 세 종류의 층(layer)이 있습니다.

입력층(input layer) 은 엄밀히 말해 계산하는 층이 아닙니다. 원시 데이터(raw data)를 담습니다. 특성(feature)이 두 개라면 입력 노드(input node)도 두 개입니다.

은닉층(hidden layer) 은 실제 계산이 일어나는 곳입니다. 각 뉴런은 이전 층의 모든 출력을 받아 가중치와 편향을 적용한 뒤 활성화 함수에 통과시킵니다. 학습 데이터에서 이 값을 직접 관찰하지 않기 때문에 "숨겨진(hidden)"이라고 부릅니다.

출력층(output layer) 은 최종 답입니다. 이진 분류(binary classification)에서는 시그모이드(sigmoid)를 사용하는 뉴런 하나를 둡니다. 다중 클래스 분류(multi-class classification)에서는 클래스마다 하나의 뉴런을 둡니다.

graph LR
    subgraph Input["Input Layer"]
        x1["x1"]
        x2["x2"]
    end
    subgraph Hidden["Hidden Layer (3 neurons)"]
        h1["h1"]
        h2["h2"]
        h3["h3"]
    end
    subgraph Output["Output Layer"]
        y["y"]
    end
    x1 --> h1
    x1 --> h2
    x1 --> h3
    x2 --> h1
    x2 --> h2
    x2 --> h3
    h1 --> y
    h2 --> y
    h3 --> y

이것은 2-3-1 네트워크입니다. 입력 2개, 은닉 뉴런(hidden neuron) 3개, 출력 1개입니다. 모든 연결에는 가중치가 있고, 입력을 제외한 모든 뉴런에는 편향(bias)이 있습니다.

각 층은 은닉 상태(hidden state)라는 숫자 벡터를 만듭니다. 텍스트에서는 단어를 768개 숫자로 인코딩(encoding)해 의미를 담는 식으로 차원이 늘어날 수 있습니다. 이미지에서는 수백만 픽셀(pixel)을 다루기 쉬운 표현으로 압축하며 차원이 줄어들 수 있습니다. 학습이 실제로 일어나는 표현은 바로 이 은닉 상태 안에 있습니다.

뉴런과 활성화 함수

각 뉴런은 세 가지를 수행합니다.

  1. 모든 입력에 대응하는 가중치를 곱합니다.
  2. 모든 곱을 더하고 편향을 더합니다.
  3. 합을 활성화 함수(activation function)에 통과시킵니다.

지금은 시그모이드(sigmoid)를 사용합니다.

sigmoid(z) = 1 / (1 + e^(-z))

시그모이드는 어떤 숫자든 (0, 1) 범위로 눌러 줍니다. 큰 양수는 1에 가까워지고, 큰 음수는 0에 가까워지며, 0은 0.5가 됩니다. 이 부드러운 곡선이 학습을 가능하게 합니다. 퍼셉트론의 딱딱한 계단 함수(step function)와 달리 시그모이드는 모든 지점에서 기울기(gradient)를 가집니다.

순전파: 데이터가 흐르는 방식

순전파(forward pass)는 입력 데이터를 네트워크에 넣고, 층마다 통과시켜 출력에 도달하게 합니다. 순전파 중에는 학습이 일어나지 않습니다. 곱하고, 더하고, 활성화하고, 반복하는 순수 계산일 뿐입니다.

graph TD
    X["Input: [x1, x2]"] --> WH["Weight Matrix W1 (2x3)를 곱함"]
    WH --> BH["Bias Vector b1 (3,)를 더함"]
    BH --> AH["각 원소에 sigmoid 적용"]
    AH --> H["Hidden Output: [h1, h2, h3]"]
    H --> WO["Weight Matrix W2 (3x1)를 곱함"]
    WO --> BO["Bias Vector b2 (1,)를 더함"]
    BO --> AO["sigmoid 적용"]
    AO --> Y["Output: y"]

각 층에서는 세 연산이 순서대로 일어납니다.

z = W * input + b       (선형 변환; linear transformation)
a = sigmoid(z)          (활성화; activation)

한 층의 출력은 다음 층의 입력이 됩니다. 이것이 순전파 전체입니다.

행렬 차원

차원을 추적하는 일은 딥러닝(deep learning)에서 가장 중요한 디버깅(debugging) 기술입니다. 2-3-1 네트워크의 차원은 다음과 같습니다.

단계연산차원결과 형태(shape)
입력(Input)x--(2,)
은닉 선형(Hidden linear)W1 * x + b1W1: (3, 2), b1: (3,)(3,)
은닉 활성화(Hidden activation)sigmoid(z1)--(3,)
출력 선형(Output linear)W2 * h + b2W2: (1, 3), b2: (1,)(1,)
출력 활성화(Output activation)sigmoid(z2)--(1,)

규칙은 이렇습니다. 층 k의 가중치 행렬(weight matrix) W는 (현재 층의 뉴런 수, 이전 층의 뉴런 수) 형태를 가집니다. 행(row)은 현재 층과 맞고, 열(column)은 이전 층과 맞습니다. 형태가 맞지 않으면 버그입니다.

보편 근사 정리

1989년 조지 시벤코(George Cybenko)는 놀라운 사실을 증명했습니다. 은닉층 하나와 충분히 많은 뉴런을 가진 신경망은 어떤 연속 함수(continuous function)라도 원하는 정확도까지 근사할 수 있습니다. 이것이 보편 근사 정리(Universal Approximation Theorem)입니다.

이 말은 은닉층 하나가 항상 최선이라는 뜻이 아닙니다. 그런 구조(architecture)가 이론적으로 가능하다는 뜻입니다. 실제로는 더 깊은 네트워크, 즉 더 많은 층과 층당 더 적은 뉴런을 가진 네트워크가 같은 함수를 훨씬 적은 매개변수(parameter)로 학습하는 경우가 많습니다. 이것이 딥러닝이 작동하는 이유입니다.

직관은 이렇습니다. 은닉층의 각 뉴런은 하나의 "봉우리(bump)" 또는 특성을 학습합니다. 올바른 위치에 충분한 봉우리를 놓으면 어떤 매끄러운 곡선도 근사할 수 있습니다. 뉴런이 많을수록 봉우리가 많아지고, 근사가 좋아집니다.

graph LR
    subgraph FewNeurons["4 Hidden Neurons"]
        A["거친 근사"]
    end
    subgraph MoreNeurons["16 Hidden Neurons"]
        B["가까운 근사"]
    end
    subgraph ManyNeurons["64 Hidden Neurons"]
        C["거의 완벽한 적합"]
    end
    FewNeurons --> MoreNeurons --> ManyNeurons

조합 가능성

신경망은 조합 가능합니다(composable). 쌓고, 연결하고, 병렬로 실행할 수 있습니다. Whisper 모델은 오디오(audio)를 처리하는 인코더 네트워크(encoder network)와 텍스트를 생성하는 별도의 디코더 네트워크(decoder network)를 사용합니다. 현대의 대규모 언어 모델(LLM)은 디코더 전용(decoder-only) 구조가 많습니다. BERT는 인코더 전용(encoder-only)입니다. T5는 인코더-디코더(encoder-decoder)입니다. 구조 선택은 모델이 할 수 있는 일을 정의합니다.

만들어 보기

순수 Python으로 작성합니다. numpy는 쓰지 않습니다. 모든 행렬 연산(matrix operation)을 처음부터 작성합니다.

Step 1: 시그모이드 활성화(Sigmoid activation)

import math

def sigmoid(x):
    x = max(-500.0, min(500.0, x))
    return 1.0 / (1.0 + math.exp(-x))

[-500, 500]으로 자르는(clamp) 이유는 오버플로(overflow)를 막기 위해서입니다. math.exp(500)은 매우 크지만 유한한 값(finite)입니다. math.exp(1000)은 무한대(infinity)가 됩니다.

Step 2: Layer 클래스

딥러닝에서 가장 중요한 연산은 행렬 곱셈(matrix multiplication)입니다. 모든 층, 모든 어텐션 헤드(attention head), 모든 순전파는 결국 행렬 곱(matmul) 위에 올라타 있습니다. 선형 층(linear layer)은 입력 벡터(input vector)에 가중치 행렬을 곱하고 편향 벡터(bias vector)를 더합니다. 식으로는 y = Wx + b입니다. 이 한 줄짜리 방정식이 신경망 계산(neural network compute)의 대부분을 차지합니다.

층은 가중치 행렬과 편향 벡터를 가집니다. forward 메서드(method)는 입력 벡터를 받아 활성화된 출력(activated output)을 반환합니다.

class Layer:
    def __init__(self, n_inputs, n_neurons, weights=None, biases=None):
        if weights is not None:
            self.weights = weights
        else:
            import random
            self.weights = [
                [random.uniform(-1, 1) for _ in range(n_inputs)]
                for _ in range(n_neurons)
            ]
        if biases is not None:
            self.biases = biases
        else:
            self.biases = [0.0] * n_neurons

    def forward(self, inputs):
        self.last_input = inputs
        self.last_output = []
        for neuron_idx in range(len(self.weights)):
            z = sum(
                w * x for w, x in zip(self.weights[neuron_idx], inputs)
            )
            z += self.biases[neuron_idx]
            self.last_output.append(sigmoid(z))
        return self.last_output

가중치 행렬의 형태는 (n_neurons, n_inputs)입니다. 각 행은 하나의 뉴런이 모든 입력에 대해 가지는 가중치입니다.

Step 3: Network 클래스

네트워크는 층의 리스트(list)입니다. 순전파는 층을 순서대로 연결합니다.

class Network:
    def __init__(self, layers):
        self.layers = layers

    def forward(self, inputs):
        current = inputs
        for layer in self.layers:
            current = layer.forward(current)
        return current

이 네 줄이 전체 순전파입니다. 데이터가 들어가고, 모든 층을 통과하고, 반대편으로 나옵니다.

Step 4: 손으로 조정한 가중치로 XOR 풀기

Lesson 01에서는 OR, NAND, AND 퍼셉트론을 조합해 XOR을 풀었습니다. 이제 LayerNetwork 클래스로 같은 일을 합니다. 2-2-1 구조입니다.

hidden = Layer(
    n_inputs=2,
    n_neurons=2,
    weights=[[20.0, 20.0], [-20.0, -20.0]],
    biases=[-10.0, 30.0],
)

output = Layer(
    n_inputs=2,
    n_neurons=1,
    weights=[[20.0, 20.0]],
    biases=[-30.0],
)

xor_net = Network([hidden, output])

xor_data = [
    ([0, 0], 0),
    ([0, 1], 1),
    ([1, 0], 1),
    ([1, 1], 0),
]

for inputs, expected in xor_data:
    result = xor_net.forward(inputs)
    predicted = 1 if result[0] >= 0.5 else 0
    print(f"  {inputs} -> {result[0]:.6f} (반올림: {predicted}, 기댓값: {expected})")

큰 가중치 (20, -20)는 시그모이드가 계단 함수처럼 작동하게 합니다. 첫 번째 은닉 뉴런은 OR에 가깝고, 두 번째는 NAND에 가깝습니다. 출력 뉴런은 둘을 AND로 조합해 XOR을 만듭니다.

Step 5: 원 분류(Circle classification)

조금 더 어려운 문제입니다. 2차원 점(2D point)이 원점 중심 반지름 0.5인 원 안에 있는지 바깥에 있는지 분류합니다. 이 문제는 곡선형 결정 경계가 필요하므로 단일 퍼셉트론으로는 불가능합니다.

import random
import math

random.seed(42)

data = []
for _ in range(200):
    x = random.uniform(-1, 1)
    y = random.uniform(-1, 1)
    label = 1 if (x * x + y * y) < 0.25 else 0
    data.append(([x, y], label))

circle_net = Network([
    Layer(n_inputs=2, n_neurons=8),
    Layer(n_inputs=8, n_neurons=1),
])

무작위 가중치(random weights)로는 잘 분류하지 못합니다. 하지만 순전파 자체는 실행됩니다. 이것이 핵심입니다. 순전파는 계산일 뿐입니다. 올바른 가중치를 학습하는 일은 Lesson 03의 역전파(backpropagation)에서 다룹니다.

correct = 0
for inputs, expected in data:
    result = circle_net.forward(inputs)
    predicted = 1 if result[0] >= 0.5 else 0
    if predicted == expected:
        correct += 1

print(f"무작위 가중치 정확도: {correct}/{len(data)} ({100*correct/len(data):.1f}%)")

무작위 가중치는 정확도가 낮습니다. 대체로 다수 클래스(majority class)를 찍는 것보다 나쁠 수 있습니다. 하지만 Lesson 03에서 학습한 뒤에는 같은 8개 은닉 뉴런 구조가 안쪽과 바깥쪽을 나누는 곡선형 경계(curved boundary)를 그릴 수 있습니다.

사용하기

PyTorch는 위 내용을 네 줄로 처리합니다.

import torch
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(2, 8),
    nn.Sigmoid(),
    nn.Linear(8, 1),
    nn.Sigmoid(),
)

x = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])
output = model(x)
print(output)

nn.Linear(2, 8)은 여러분의 Layer 클래스와 같습니다. 형태 (8, 2)의 가중치 행렬과 형태 (8,)의 편향 벡터를 가집니다. nn.Sigmoid()는 시그모이드 함수를 요소별(element-wise)로 적용합니다. nn.Sequential은 여러분의 Network 클래스처럼 층을 순서대로 연결합니다.

차이는 속도와 규모(scale)입니다. PyTorch는 GPU에서 실행되고, 수백만 개의 표본 배치(sample batch)를 다루며, 역전파를 위한 기울기를 자동으로 계산합니다. 그러나 순전파의 논리(logic)는 방금 직접 만든 것과 같습니다.

산출물 만들기

이 lesson은 네트워크 구조를 설계할 때 재사용할 수 있는 프롬프트(prompt)를 만듭니다.

  • outputs/prompt-network-architect.md

문제에 맞는 층 수, 층별 뉴런 수, 활성화 함수를 결정해야 할 때 사용합니다.

연습문제

  1. 두 개의 은닉층을 가진 2-4-2-1 네트워크를 만들고 XOR 데이터에 대해 순전파를 실행합니다. 중간 은닉층 출력을 출력해 표현이 층마다 어떻게 바뀌는지 확인합니다.
  2. 원 분류기(circle classifier)의 은닉층 크기를 8에서 2로, 그리고 32로 바꿉니다. 매번 무작위 가중치로 순전파를 실행합니다. 은닉 뉴런 수가 출력 범위(output range)나 분포(distribution)를 바꾸나요? 이유는 무엇인가요?
  3. Network 클래스에 학습 가능한 가중치와 편향의 총개수를 반환하는 count_parameters 메서드를 구현합니다. 고전적인 MNIST 구조인 784-256-128-10 네트워크에서 시험합니다. 매개변수는 몇 개인가요?
  4. 3-4-4-2 네트워크의 순전파를 만듭니다. 0-1로 정규화(normalize)한 RGB 색상 값을 입력하고 두 출력을 관찰합니다. 이것은 두 클래스를 가진 간단한 색상 분류기(color classifier) 구조입니다.
  5. 시그모이드를 "누설 계단(leaky step)" 함수로 바꿉니다. z < 0이면 0.01 * z, 아니면 1.0을 반환합니다. Step 4의 손으로 조정한 가중치로 XOR 순전파를 실행합니다. 여전히 작동하나요? 딱딱한 절단(hard cutoff)보다 매끄러운 시그모이드(smooth sigmoid)가 선호되는 이유는 무엇인가요?

핵심 용어

용어흔한 설명실제 의미
순전파(Forward pass)"모델을 실행하는 것"입력을 모든 층에 통과시켜 출력을 만드는 계산 과정
은닉층(Hidden layer)"중간 부분"입력과 출력 사이에 있으며 데이터에서 직접 관찰되지 않는 값을 만드는 층
다층 네트워크(Multi-layer network)"심층 신경망(deep neural network)"각 층의 출력이 다음 층의 입력이 되도록 뉴런 층을 순서대로 쌓은 구조
활성화 함수(Activation function)"비선형성(nonlinearity)"선형 변환 뒤에 적용되어 결정 경계에 곡선을 만드는 함수
시그모이드(Sigmoid)"S자 곡선(S-curve)"sigma(z) = 1/(1+e^(-z)), 실수를 (0,1)로 압축하는 매끄럽고 미분 가능한(smooth and differentiable) 함수
가중치 행렬(Weight matrix)"매개변수(parameter)"형태 (current_layer_neurons, previous_layer_neurons)의 학습 가능한 연결 강도(learnable connection strength)
편향 벡터(Bias vector)"오프셋(offset)"행렬 곱 뒤에 더해져 모든 입력이 0이어도 뉴런이 활성화될 수 있게 하는 벡터
보편 근사(Universal approximation)"신경망은 무엇이든 학습한다"충분한 뉴런을 가진 단일 은닉층이 임의의 연속 함수(continuous function)를 근사할 수 있다는 정리
선형 변환(Linear transformation)"행렬 곱 단계(matrix multiply step)"활성화 전 계산인 z = W * x + b
결정 경계(Decision boundary)"분류기가 바뀌는 위치"네트워크 출력이 분류 임계값(classification threshold)을 지나는 입력 공간(input space)의 곡면

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

prompt-network-architect

Guides the user through designing neural network architectures by choosing layer counts, neuron counts, and activation functions for a given problem

Prompt

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.2개의 뉴런(neuron)에서 입력을 받는 3개 뉴런 층의 가중치 행렬(weight matrix) 형태(shape)는 무엇인가요?

2.보편 근사 정리(Universal Approximation Theorem)는 무엇을 보장하나요?

3.퍼셉트론(perceptron)에서 쓰는 계단 함수(step function)와 달리 시그모이드(sigmoid) 활성화 함수가 학습을 가능하게 하는 이유는 무엇인가요?

0/3 답변 완료