튜토리얼
12. Neural Network

12. Neural Network 기초

Perceptron, Backpropagation, Activation Functions


학습 목표

이 튜토리얼을 완료하면 다음을 이해할 수 있습니다:

  • 신경망의 기본 구조 (뉴런, 레이어) 이해
  • 활성화 함수 (Sigmoid, ReLU, Tanh, Softmax)의 역할과 특징
  • 순전파 (Forward Propagation) 과정
  • 역전파 (Backpropagation)와 Chain Rule
  • 경사하강법을 통한 신경망 학습
  • XOR 문제 해결을 통한 비선형 분류 이해

핵심 개념

1. Neural Network란?

뇌의 뉴런 구조를 모방한 머신러닝 모델입니다. 입력층, 은닉층, 출력층으로 구성되며, 각 층의 뉴런들이 연결되어 복잡한 패턴을 학습합니다.

입력층       은닉층       출력층

  x1 ────┬───► h1 ────┬───► y
         │           │
  x2 ────┼───► h2 ────┤
         │           │
         └───► h3 ────┘

2. Perceptron (단일 뉴런)

퍼셉트론은 가장 단순한 형태의 신경망으로, 입력에 가중치를 곱하고 편향을 더한 후 활성화 함수를 적용합니다.

z = w₁x₁ + w₂x₂ + ... + wₙxₙ + b
y = activation(z)
class Perceptron:
    def __init__(self, n_features, learning_rate=0.1):
        self.weights = np.random.randn(n_features) * 0.1
        self.bias = 0
        self.lr = learning_rate
 
    def step_function(self, z):
        return (z >= 0).astype(int)
 
    def predict(self, X):
        z = np.dot(X, self.weights) + self.bias
        return self.step_function(z)
 
    def fit(self, X, y, epochs=100):
        for epoch in range(epochs):
            errors = 0
            for xi, yi in zip(X, y):
                prediction = self.predict(xi.reshape(1, -1))[0]
                error = yi - prediction
 
                # 가중치 업데이트
                self.weights += self.lr * error * xi
                self.bias += self.lr * error
                errors += abs(error)
 
            if errors == 0:
                print(f'Epoch {epoch+1}: 수렴!')
                break
        return self
⚠️

단일 퍼셉트론은 선형 분리 가능한 문제만 해결할 수 있습니다. AND, OR 게이트는 학습 가능하지만, XOR 문제는 해결할 수 없습니다.


3. Activation Functions

활성화 함수는 뉴런의 출력을 비선형으로 변환하여 신경망이 복잡한 패턴을 학습할 수 있게 합니다.

함수공식범위특징
Sigmoid1/(1+e^(-z))(0, 1)확률 출력, Vanishing Gradient
Tanh(e^z - e^(-z))/(e^z + e^(-z))(-1, 1)0 중심, Vanishing Gradient
ReLUmax(0, z)[0, ∞)기본 선택, Dead ReLU 문제
Leaky ReLUmax(0.01z, z)(-∞, ∞)Dead ReLU 해결
Softmaxe^zᵢ / Σe^zⱼ(0, 1)다중 분류 출력
def sigmoid(x):
    return 1 / (1 + np.exp(-np.clip(x, -500, 500)))
 
def sigmoid_derivative(x):
    s = sigmoid(x)
    return s * (1 - s)
 
def relu(x):
    return np.maximum(0, x)
 
def relu_derivative(x):
    return (x > 0).astype(float)
 
def leaky_relu(x, alpha=0.01):
    return np.where(x > 0, x, alpha * x)
 
def softmax(z):
    exp_z = np.exp(z - np.max(z))  # 수치 안정성
    return exp_z / exp_z.sum()

활성화 함수 선택 가이드:

  • 은닉층: ReLU (기본), Leaky ReLU (Dead neuron 방지)
  • 이진 분류 출력층: Sigmoid
  • 다중 분류 출력층: Softmax
  • 회귀 출력층: Linear (활성화 없음)

4. Forward Propagation (순전파)

입력에서 출력으로 순차적으로 계산하는 과정입니다. 각 층에서 가중합을 계산하고 활성화 함수를 적용합니다.

def forward(X, W1, b1, W2, b2):
    # Hidden layer
    z1 = X @ W1 + b1
    a1 = relu(z1)
 
    # Output layer
    z2 = a1 @ W2 + b2
    a2 = sigmoid(z2)
 
    return a2

5. Backpropagation (역전파)

출력에서 입력으로 역방향으로 그래디언트를 계산합니다. Chain Rule을 사용하여 각 가중치에 대한 손실 함수의 기울기를 구합니다.

Chain Rule:

∂L/∂w = ∂L/∂y × ∂y/∂z × ∂z/∂w

2층 신경망 예시:

x → [W1] → z1 → [σ] → a1 → [W2] → z2 → [σ] → a2 → L

LW2=La2a2z2z2W2\frac{\partial L}{\partial W_2} = \frac{\partial L}{\partial a_2} \cdot \frac{\partial a_2}{\partial z_2} \cdot \frac{\partial z_2}{\partial W_2}

LW1=La2a2z2z2a1a1z1z1W1\frac{\partial L}{\partial W_1} = \frac{\partial L}{\partial a_2} \cdot \frac{\partial a_2}{\partial z_2} \cdot \frac{\partial z_2}{\partial a_1} \cdot \frac{\partial a_1}{\partial z_1} \cdot \frac{\partial z_1}{\partial W_1}

def backprop(X, y, a1, a2, W2):
    m = len(y)
 
    # Output layer gradient
    dz2 = a2 - y
    dW2 = (1/m) * a1.T @ dz2
    db2 = (1/m) * np.sum(dz2, axis=0)
 
    # Hidden layer gradient
    dz1 = (dz2 @ W2.T) * relu_derivative(a1)
    dW1 = (1/m) * X.T @ dz1
    db1 = (1/m) * np.sum(dz1, axis=0)
 
    return dW1, db1, dW2, db2

6. 다층 신경망 (MLP) 구현

XOR 문제를 해결하려면 은닉층이 필요합니다. 다음은 완전한 다층 신경망 구현입니다:

class NeuralNetwork:
    def __init__(self, layer_sizes, learning_rate=0.1, activation='sigmoid'):
        """
        layer_sizes: [입력, 은닉1, 은닉2, ..., 출력]
        예: [2, 4, 1] = 2 입력, 4 은닉, 1 출력
        """
        self.layer_sizes = layer_sizes
        self.lr = learning_rate
        self.n_layers = len(layer_sizes)
 
        # Xavier initialization
        self.weights = []
        self.biases = []
 
        for i in range(self.n_layers - 1):
            w = np.random.randn(layer_sizes[i], layer_sizes[i+1]) * np.sqrt(2.0 / layer_sizes[i])
            b = np.zeros((1, layer_sizes[i+1]))
            self.weights.append(w)
            self.biases.append(b)
 
    def forward(self, X):
        """순전파"""
        self.activations = [X]
        self.z_values = []
 
        A = X
        for i in range(self.n_layers - 1):
            Z = np.dot(A, self.weights[i]) + self.biases[i]
            self.z_values.append(Z)
 
            if i == self.n_layers - 2:
                A = sigmoid(Z)  # 출력층
            else:
                A = relu(Z)  # 은닉층
 
            self.activations.append(A)
 
        return A
 
    def fit(self, X, y, epochs=1000):
        """학습"""
        for epoch in range(epochs):
            output = self.forward(X)
            loss = -np.mean(y * np.log(output + 1e-8) + (1 - y) * np.log(1 - output + 1e-8))
            self.backward(X, y)
        return self

Xavier/He 초기화: 가중치를 적절한 분산으로 초기화하면 학습이 안정적으로 진행됩니다. np.sqrt(2.0 / n_input)을 사용하는 He 초기화는 ReLU와 함께 사용하기 좋습니다.


7. 손실 함수

문제손실 함수
이진 분류Binary Cross-Entropy
다중 분류Categorical Cross-Entropy
회귀MSE
# Binary Cross-Entropy
def bce_loss(y_true, y_pred):
    epsilon = 1e-15
    y_pred = np.clip(y_pred, epsilon, 1 - epsilon)
    return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))

8. Optimizer

Optimizer특징
SGD기본, 느림
Momentum관성 추가, 수렴 속도 개선
AdamAdaptive + Momentum (기본 선택)
RMSpropAdaptive learning rate
⚠️

학습률 설정 주의:

  • 너무 작으면: 학습이 매우 느림
  • 너무 크면: 발산하거나 진동
  • 권장: 0.001 ~ 0.01로 시작하여 조정

9. Vanishing Gradient 문제

깊은 신경망에서 Sigmoid나 Tanh를 사용하면 역전파 시 기울기가 점점 작아져 학습이 어려워집니다.

🚫

기울기 소실 (Vanishing Gradient): Sigmoid의 최대 기울기는 0.25입니다. 5개 층을 통과하면 기울기가 0.25^5 ≈ 0.001로 감소합니다.

해결책:

  • ReLU 활성화 함수 사용
  • Batch Normalization
  • Residual Connection (Skip Connection)
  • 적절한 가중치 초기화

Keras/TensorFlow 코드

import tensorflow as tf
from tensorflow.keras import layers, models
 
# 모델 정의
model = models.Sequential([
    layers.Dense(64, activation='relu', input_shape=(n_features,)),
    layers.Dense(32, activation='relu'),
    layers.Dense(1, activation='sigmoid')
])
 
# 컴파일
model.compile(
    optimizer='adam',
    loss='binary_crossentropy',
    metrics=['accuracy']
)
 
# 학습
history = model.fit(
    X_train, y_train,
    epochs=100,
    batch_size=32,
    validation_split=0.2,
    verbose=1
)
 
# 평가
loss, accuracy = model.evaluate(X_test, y_test)

과적합 방지

Dropout

학습 시 무작위로 뉴런을 비활성화하여 과적합을 방지합니다.

model = models.Sequential([
    layers.Dense(64, activation='relu'),
    layers.Dropout(0.3),  # 30% 뉴런 비활성화
    layers.Dense(1, activation='sigmoid')
])

Early Stopping

검증 손실이 더 이상 개선되지 않으면 학습을 조기 종료합니다.

from tensorflow.keras.callbacks import EarlyStopping
 
early_stop = EarlyStopping(
    monitor='val_loss',
    patience=10,
    restore_best_weights=True
)
 
model.fit(X_train, y_train, callbacks=[early_stop])

L2 Regularization

가중치의 크기를 제한하여 과적합을 방지합니다.

from tensorflow.keras import regularizers
 
layers.Dense(64, activation='relu',
             kernel_regularizer=regularizers.l2(0.01))

하이퍼파라미터 가이드

파라미터권장값설명
Hidden units32, 64, 128, 256문제 복잡도에 따라 조정
Learning rate0.001, 0.01Adam 기본값 0.001 권장
Batch size32, 64, 128메모리와 수렴 속도 고려
Dropout0.2 ~ 0.5과적합 정도에 따라 조정
Epochs100 ~ 1000Early Stopping과 함께 사용

면접 질문 맛보기

  1. Backpropagation의 원리는?
  2. ReLU의 장점과 단점은?
  3. Vanishing/Exploding Gradient 문제와 해결책은?
  4. 단일 퍼셉트론이 XOR 문제를 풀 수 없는 이유는?
  5. Xavier/He 초기화를 사용하는 이유는?

더 많은 면접 질문은 Premium Interviews (opens in a new tab)에서 확인하세요.


실습 노트북

노트북 추가 내용: 실습 노트북에서는 AND/OR/XOR 게이트 실험, Moon 데이터셋 분류, 학습률과 은닉층 크기에 따른 성능 비교, Scikit-learn MLPClassifier와의 비교, 그리고 결정 경계 시각화 등 더 심화된 내용을 다룹니다.


이전: 11. Time Series | 다음: 13. CNN