뉴럴 네트워크의 기초: 완벽 가이드
1. 뉴럴 네트워크란?
1.1 기본 개념
뉴럴 네트워크를 이해하기 위해 먼저 선형 분류기를 살펴보겠습니다. 기존의 선형 분류에서는 다음과 같은 공식을 사용했습니다:
s = Wx # 여기서 s는 점수, W는 가중치 행렬, x는 입력 벡터
예를 들어 CIFAR-10 데이터셋의 경우:
- x: [3072x1] 크기의 이미지 픽셀 데이터 벡터
- W: [10x3072] 크기의 가중치 행렬
- s: 10개 클래스에 대한 점수 벡터
1.2 뉴럴 네트워크의 기본 수식
기본적인 뉴럴 네트워크는 다음과 같은 형태를 가집니다:
s = W2 * max(0, W1x)
여기서:
- W1: [100x3072] 크기의 첫 번째 가중치 행렬
- max(0,-): ReLU 활성화 함수
- W2: [10x100] 크기의 두 번째 가중치 행렬
3층 신경망의 경우:
s = W3 * max(0, W2 * max(0, W1x))
2. 생물학적 뉴런과 인공 뉴런
2.1 생물학적 뉴런의 구조
인간의 신경계는 놀라운 복잡성을 가지고 있습니다:
- 약 860억 개의 뉴런
- 10^14 - 10^15개의 시냅스 연결
- 구성 요소:
- 수상돌기: 입력 신호 수신
- 세포체: 신호 처리
- 축삭: 출력 신호 전달
- 시냅스: 뉴런 간 연결 지점
2.2 수학적 모델링
실제 뉴런을 단순화하여 다음과 같이 모델링합니다:
class Neuron(object):
def __init__(self, weights, bias):
self.weights = weights
self.bias = bias
def forward(self, inputs):
# 1. 입력과 가중치의 곱의 합
cell_body_sum = np.sum(inputs * self.weights) + self.bias
# 2. 활성화 함수 적용
firing_rate = 1.0 / (1.0 + math.exp(-cell_body_sum))
return firing_rate
이 모델의 특징:
- 입력 신호와 가중치의 곱
- 바이어스 항 추가
- 활성화 함수를 통한 출력 생성
3. 활성화 함수의 종류와 특징
3.1 시그모이드 (Sigmoid)
def sigmoid(x):
return 1 / (1 + np.exp(-x))
특징:
- 출력 범위: [0, 1]
- 장점:
- 생물학적 뉴런의 발화율과 유사
- 확률로 해석 가능
- 단점:
- 기울기 소실 문제
- zero-centered가 아님
- 실제 학습에서 성능 저하
3.2 하이퍼볼릭 탄젠트 (tanh)
def tanh(x):
return (np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x))
특징:
- 출력 범위: [-1, 1]
- 장점:
- zero-centered 출력
- 시그모이드보다 더 나은 학습 성능
- 단점:
- 여전히 기울기 소실 문제 존재
3.3 ReLU (Rectified Linear Unit)
def relu(x):
return max(0, x)
특징:
- 출력 범위: [0, ∞)
- 장점:
- 계산 효율성
- 기울기 소실 문제 감소
- 학습 속도 향상 (약 6배)
- 단점:
- Dead ReLU 문제
- 학습률 설정 중요
3.4 Leaky ReLU
특징:
- Dead ReLU 문제 해결
- 음수 입력에 대해 작은 기울기 제공
- alpha 값을 학습 가능하게 설정 가능 (PReLU)
4. 네트워크 아키텍처 설계
4.1 레이어 구성
기본 구성:
- 입력 레이어
- 은닉 레이어 (여러 개 가능)
- 출력 레이어
예시 코드:
class NeuralNetwork:
def __init__(self, layer_sizes):
self.weights = []
self.biases = []
for i in range(len(layer_sizes)-1):
w = np.random.randn(layer_sizes[i+1], layer_sizes[i])
b = np.random.randn(layer_sizes[i+1], 1)
self.weights.append(w)
self.biases.append(b)
4.2 순전파 연산
def forward(self, x):
activation = x
activations = [x]
for w, b in zip(self.weights, self.biases):
z = np.dot(w, activation) + b
activation = relu(z) # ReLU 활성화 함수 사용
activations.append(activation)
return activations
5. 네트워크 크기 결정
5.1 은닉층 크기 선정
고려사항:
- 데이터 복잡도
- 계산 자원
- 과적합 위험
일반적 지침:
- 입력층의 1.5~2배 크기로 시작
- 성능에 따라 조정
- 여러 개의 작은 층보다 하나의 큰 층이 더 효과적일 수 있음
5.2 과적합 제어
방법:
- L2 정규화
loss += lambda_ * np.sum(w * w for w in weights)
2. 드롭아웃
def dropout(x, rate=0.5):
mask = np.random.binomial(1, rate, size=x.shape)
return x * mask
- 데이터 증강
- 조기 종료
6. 실전 최적화 팁
6.1 초기화
def he_initialization(layer_size):
return np.random.randn(layer_size) * np.sqrt(2.0/layer_size)
6.2 학습률 설정
- 시작값: 0.01
- 학습 곡선 모니터링
- 필요시 학습률 감소
6.3 미니배치 크기
- 일반적으로 32~256
- GPU 메모리 고려
- 더 큰 배치가 항상 좋은 것은 아님
결론
뉴럴 네트워크의 성공적인 구현을 위한 핵심 포인트:
- ReLU 활성화 함수 사용
- 충분히 큰 네트워크 설계
- 적절한 정규화 적용
- 학습 과정 모니터링
- 하이퍼파라미터 최적화
실제 구현시 주의사항:
- 데이터 전처리 중요성
- 과적합 신중하게 모니터링
- 계산 효율성 고려
- 모델 평가 지표 선정
뉴럴 네트워크의 실전 구현 가이드: 데이터 전처리부터 모델 설정까지
1. 데이터 전처리 (Data Preprocessing)
1.1 일반적인 전처리 방법
데이터 행렬 X가 [N x D] 크기일 때 (N: 데이터 수, D: 차원) 세 가지 주요 전처리 방법이 있습니다:
1) 평균 제거 (Mean Subtraction)
# 각 특성별 평균 제거
X -= np.mean(X, axis=0)
# 이미지의 경우 전체 픽셀에서 단일 값 제거도 가능
X -= np.mean(X)
장점:
- 가장 일반적인 전처리 방법
- 데이터를 각 차원에서 원점 중심으로 이동
- 기하학적 해석이 용이
2) 정규화 (Normalization)
# 표준편차로 나누기
X /= np.std(X, axis=0)
# 또는 min-max 스케일링 (-1에서 1 사이로)
X = (X - X.min()) / (X.max() - X.min()) * 2 - 1
적용 시기:
- 입력 특성들의 스케일이 다를 때
- 모든 특성이 동일한 중요도를 가져야 할 때
3) PCA와 백색화 (PCA and Whitening)
# 1. 데이터 중심화
X -= np.mean(X, axis=0)
# 2. 공분산 행렬 계산
cov = np.dot(X.T, X) / X.shape[0]
# 3. SVD 분해
U, S, V = np.linalg.svd(cov)
# 4. 데이터 회전
Xrot = np.dot(X, U)
# 5. 백색화
Xwhite = Xrot / np.sqrt(S + 1e-5)
특징:
- 데이터의 상관관계 제거
- 차원 축소 가능
- 노이즈가 과도하게 강조될 수 있음
1.2 실전 팁
- 전처리 통계는 훈련 데이터에서만 계산
- 검증/테스트 데이터에는 훈련 데이터의 통계를 적용
- 합성곱 신경망(CNN)에서는 PCA/백색화 잘 사용하지 않음
- 데이터 중심화는 매우 중요
2. 가중치 초기화 (Weight Initialization)
2.1 잘못된 방법
W = 0 # 모든 가중치를 0으로 초기화 (사용하면 안 됨)
문제점:
- 모든 뉴런이 동일한 출력 생성
- 역전파 시 동일한 그래디언트 계산
- 뉴런 간 비대칭성 없음
2.2 올바른 초기화 방법
1) 작은 랜덤 값 사용
W = 0.01 * np.random.randn(D, H)
2) Xavier 초기화
W = np.random.randn(n) / np.sqrt(n)
3) He 초기화 (ReLU용)
W = np.random.randn(n) * np.sqrt(2.0/n)
2.3 바이어스 초기화
b = 0 # 일반적으로 0으로 초기화
# 또는 ReLU의 경우
b = 0.01 # 작은 양수값
3. 정규화 (Regularization)
3.1 L2 정규화
loss = data_loss + 0.5 * lambda_reg * np.sum(W * W)
특징:
- 가장 일반적인 정규화 방법
- 가중치 벡터를 작고 분산된 값으로 유지
3.2 L1 정규화
loss = data_loss + lambda_reg * np.sum(np.abs(W))
특징:
- 희소한 가중치 벡터 생성
- 특성 선택에 유용
3.3 드롭아웃 (Dropout)
일반 드롭아웃
def train_step(X):
H1 = np.maximum(0, np.dot(W1, X) + b1)
U1 = np.random.rand(*H1.shape) < p
H1 *= U1 # 드롭아웃 적용
def predict(X):
H1 = np.maximum(0, np.dot(W1, X) + b1) * p
권장되는 Inverted Dropout
def train_step(X):
H1 = np.maximum(0, np.dot(W1, X) + b1)
U1 = (np.random.rand(*H1.shape) < p) / p
H1 *= U1
def predict(X):
H1 = np.maximum(0, np.dot(W1, X) + b1) # 스케일링 필요 없음
4. 손실 함수 (Loss Functions)
4.1 분류 문제
# SVM 손실
loss = np.sum(np.maximum(0, scores - scores[y] + 1))
# Softmax 손실
softmax = np.exp(scores) / np.sum(np.exp(scores))
loss = -np.log(softmax[y])
4.2 회귀 문제
# L2 손실
loss = np.sum((predictions - targets) ** 2)
# L1 손실
loss = np.sum(np.abs(predictions - targets))
실전 추천 사항:
- 데이터 전처리:
- 평균이 0이 되도록 중심화
- [-1, 1] 범위로 스케일링
- 가중치 초기화:
- ReLU: He 초기화 사용
- tanh: Xavier 초기화 사용
- 정규화:
- L2 정규화와 드롭아웃 조합 사용
- 드롭아웃은 p=0.5가 기본값
- Inverted Dropout 구현 사용
- 손실 함수:
- 분류: Softmax Cross Entropy
- 회귀: 가능하면 분류로 변환 고려
- L2 손실 사용시 주의 필요
- 배치 정규화:
- 완전연결층 또는 합성곱층 후에 배치정규화 삽입
- 활성화 함수 전에 적용
딥러닝 학습 최적화 가이드
1. 그래디언트 체크 (Gradient Check)
1.1 중앙 차분법 사용
# 잘못된 방법 (Forward difference)
dx = (f(x + h) - f(x)) / h
# 올바른 방법 (Centered difference)
dx = (f(x + h) - f(x - h)) / (2*h)
1.2 상대 오차 확인
def rel_error(analytic_grad, numeric_grad):
diff = np.abs(analytic_grad - numeric_grad)
scale = np.maximum(np.abs(analytic_grad), np.abs(numeric_grad))
return diff / scale
상대 오차 기준:
- 1e-2: 그래디언트 잘못됨
- 1e-2 ~ 1e-4: 주의 필요
- 1e-4 ~ 1e-7: 대체로 양호
- < 1e-7: 매우 좋음
1.3 그래디언트 체크 팁
- 더블 정밀도(double precision) 사용
- 적은 데이터로 테스트
- h 값 신중하게 선택 (보통 1e-5)
- regularization 효과 분리
- dropout/augmentation 비활성화
2. 학습 전 기본 점검사항
2.1 초기 손실값 확인
# CIFAR-10 예시
def check_initial_loss():
# Softmax 분류기 초기 손실: 2.302 (= -ln(0.1))
# SVM 분류기 초기 손실: 9.0 (마진이 1일 때)
init_loss = model.compute_loss()
print(f"Initial loss: {init_loss}")
2.2 과적합 테스트
def overfit_small_batch():
small_data = get_small_dataset(20) # 20개 샘플
model.train(small_data, reg_strength=0)
final_loss = model.compute_loss()
assert final_loss < 1e-7 # 거의 0에 가까워야 함
3. 학습 과정 모니터링
3.1 손실 함수 모니터링
class LearningMonitor:
def __init__(self):
self.losses = []
self.train_acc = []
self.val_acc = []
def update(self, loss, train_acc, val_acc):
self.losses.append(loss)
self.train_acc.append(train_acc)
self.val_acc.append(val_acc)
def plot_loss(self):
plt.plot(self.losses)
plt.title('Training Loss')
plt.xlabel('Iteration')
plt.ylabel('Loss')
3.2 가중치/업데이트 비율 모니터링
def check_update_ratio():
param_scale = np.linalg.norm(W.ravel())
update = -learning_rate * dW
update_scale = np.linalg.norm(update.ravel())
ratio = update_scale / param_scale
print(f"Update ratio: {ratio:.2e}") # ~1e-3이 바람직
4. 파라미터 업데이트 방법
4.1 기본 SGD
def sgd_update(w, dw, learning_rate):
return w - learning_rate * dw
4.2 모멘텀 SGD
class MomentumSGD:
def __init__(self, learning_rate=0.01, momentum=0.9):
self.learning_rate = learning_rate
self.momentum = momentum
self.velocity = None
def update(self, w, dw):
if self.velocity is None:
self.velocity = np.zeros_like(w)
self.velocity = self.momentum * self.velocity - self.learning_rate * dw
return w + self.velocity
4.3 Nesterov 모멘텀
class NesterovMomentum:
def __init__(self, learning_rate=0.01, momentum=0.9):
self.learning_rate = learning_rate
self.momentum = momentum
self.velocity = None
def update(self, w, dw):
if self.velocity is None:
self.velocity = np.zeros_like(w)
v_prev = self.velocity
self.velocity = self.momentum * self.velocity - self.learning_rate * dw
w_update = -self.momentum * v_prev + (1 + self.momentum) * self.velocity
return w + w_update
4.4 Adam 옵티마이저
class Adam:
def __init__(self, learning_rate=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8):
self.learning_rate = learning_rate
self.beta1 = beta1
self.beta2 = beta2
self.epsilon = epsilon
self.m = None
self.v = None
self.t = 0
def update(self, w, dw):
if self.m is None:
self.m = np.zeros_like(w)
self.v = np.zeros_like(w)
self.t += 1
# Update biased first moment estimate
self.m = self.beta1 * self.m + (1 - self.beta1) * dw
# Update biased second moment estimate
self.v = self.beta2 * self.v + (1 - self.beta2) * dw**2
# Bias correction
m_hat = self.m / (1 - self.beta1**self.t)
v_hat = self.v / (1 - self.beta2**self.t)
update = -self.learning_rate * m_hat / (np.sqrt(v_hat) + self.epsilon)
return w + update
5. 학습률 스케줄링
5.1 단계적 감소
class StepLRScheduler:
def __init__(self, initial_lr, drop_factor=0.5, drop_every=10):
self.initial_lr = initial_lr
self.drop_factor = drop_factor
self.drop_every = drop_every
def get_lr(self, epoch):
return self.initial_lr * (self.drop_factor ** (epoch // self.drop_every))
5.2 지수적 감소
class ExponentialLRScheduler:
def __init__(self, initial_lr, decay_rate):
self.initial_lr = initial_lr
self.decay_rate = decay_rate
def get_lr(self, epoch):
return self.initial_lr * np.exp(-self.decay_rate * epoch)
6. 하이퍼파라미터 최적화
6.1 무작위 탐색 구현
def random_search():
param_distributions = {
'learning_rate': lambda: 10 ** np.random.uniform(-6, 1),
'reg_strength': lambda: 10 ** np.random.uniform(-3, 3),
'momentum': lambda: np.random.uniform(0.8, 0.999),
'dropout': lambda: np.random.uniform(0.3, 0.7)
}
best_params = None
best_val_acc = -1
for _ in range(100): # 100회 시도
params = {k: v() for k, v in param_distributions.items()}
model = train_model(**params)
val_acc = evaluate(model)
if val_acc > best_val_acc:
best_val_acc = val_acc
best_params = params
return best_params
6.2 단계적 탐색
def staged_search():
# 1단계: 넓은 범위, 적은 에폭
coarse_params = random_search(
lr_range=(-6, 1),
epochs=1
)
# 2단계: 좁은 범위, 중간 에폭
medium_params = random_search(
lr_range=(coarse_params['lr']-1, coarse_params['lr']+1),
epochs=5
)
# 3단계: 매우 좁은 범위, 많은 에폭
final_params = random_search(
lr_range=(medium_params['lr']-0.1, medium_params['lr']+0.1),
epochs=20
)
return final_params
7. 모델 앙상블
7.1 기본 앙상블
class ModelEnsemble:
def __init__(self, models):
self.models = models
def predict(self, X):
predictions = [model.predict(X) for model in self.models]
return np.mean(predictions, axis=0)
7.2 가중치 평균화
class ExponentialMovingAverageModel:
def __init__(self, model, decay=0.999):
self.model = model
self.decay = decay
self.shadow = {}
def update(self):
for name, param in self.model.named_parameters():
if name not in self.shadow:
self.shadow[name] = param.data.clone()
else:
self.shadow[name].mul_(self.decay).add_(
param.data * (1 - self.decay))
8. 최종 체크리스트
- 그래디언트 체크 수행
- 초기 손실값 확인
- 작은 데이터셋으로 과적합 테스트
- 학습 과정 모니터링 설정
- 적절한 옵티마이저 선택 (Adam 또는 Nesterov Momentum SGD)
- 학습률 스케줄링 구현
- 하이퍼파라미터 최적화 수행
- 모델 앙상블 고려