본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
AI Generated
2025. 12. 4. · 51 Views
신경망 훈련과 활성화 함수 완벽 가이드
신경망이 어떻게 학습하는지, 그리고 활성화 함수가 왜 필요한지를 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. 경사 하강법부터 역전파, 그리고 다양한 활성화 함수의 특징과 선택 기준까지 실무에 필요한 핵심 개념을 다룹니다.
목차
1. 신경망 훈련 과정
김개발 씨는 처음으로 딥러닝 프로젝트에 투입되었습니다. 선배가 건네준 코드에는 model.fit()이라는 함수가 있었는데, 이 한 줄 안에서 도대체 무슨 일이 벌어지는 걸까요?
"그냥 학습시키는 거야"라는 설명만으로는 도무지 이해가 되지 않았습니다.
신경망 훈련은 한마디로 예측값과 실제값의 차이를 줄여나가는 과정입니다. 마치 다트를 던지면서 과녁 중앙에 점점 가까워지도록 연습하는 것과 같습니다.
이 과정을 이해하면 왜 학습률이 중요한지, 왜 에포크를 여러 번 돌려야 하는지 자연스럽게 알게 됩니다.
다음 코드를 살펴봅시다.
import numpy as np
# 신경망 훈련의 기본 흐름
def train_step(X, y_true, weights, learning_rate=0.01):
# 1단계: 순전파 - 예측값 계산
y_pred = np.dot(X, weights)
# 2단계: 손실 계산 - 얼마나 틀렸는지 측정
loss = np.mean((y_pred - y_true) ** 2)
# 3단계: 기울기 계산 - 어느 방향으로 수정할지
gradient = 2 * np.dot(X.T, (y_pred - y_true)) / len(y_true)
# 4단계: 가중치 업데이트 - 실제로 수정
weights = weights - learning_rate * gradient
return weights, loss
김개발 씨는 입사 2개월 차 주니어 개발자입니다. 회사에서 처음으로 머신러닝 프로젝트에 배정받았는데, 코드를 보면 볼수록 머릿속이 복잡해졌습니다.
model.fit() 한 줄이면 학습이 된다는데, 그 안에서 도대체 무슨 마법이 일어나는 걸까요? 선배 개발자 박시니어 씨가 커피를 건네며 말했습니다.
"신경망 훈련은 생각보다 단순한 원리야. 한번 차근차근 설명해줄게." 그렇다면 신경망 훈련이란 정확히 무엇일까요?
쉽게 비유하자면, 신경망 훈련은 마치 농구 자유투 연습과 같습니다. 처음에는 공이 엉뚱한 곳으로 날아갑니다.
하지만 던질 때마다 "아, 조금 왼쪽으로 틀어졌네", "힘이 너무 셌네"라고 피드백을 받으면서 점점 정확해집니다. 신경망도 마찬가지로 예측이 틀릴 때마다 자신의 가중치를 조금씩 수정합니다.
신경망 훈련은 크게 네 단계로 이루어집니다. 첫 번째는 순전파입니다.
입력 데이터가 신경망을 통과하면서 예측값을 만들어냅니다. 마치 공장의 컨베이어 벨트처럼 데이터가 한 방향으로 흘러가면서 변환됩니다.
두 번째는 손실 계산입니다. 예측값과 실제 정답을 비교해서 얼마나 틀렸는지 숫자로 표현합니다.
이 숫자가 바로 손실값인데, 우리의 목표는 이 값을 최대한 작게 만드는 것입니다. 세 번째는 기울기 계산입니다.
손실을 줄이려면 가중치를 어느 방향으로 얼마나 수정해야 하는지 계산합니다. 이 부분이 바로 다음에 배울 역전파의 핵심입니다.
네 번째는 가중치 업데이트입니다. 계산된 기울기를 바탕으로 실제로 가중치를 수정합니다.
이때 학습률이라는 값이 중요한 역할을 합니다. 위의 코드를 살펴보겠습니다.
먼저 y_pred = np.dot(X, weights) 부분에서 순전파가 일어납니다. 입력과 가중치를 곱해서 예측값을 만듭니다.
그 다음 loss 계산에서는 평균 제곱 오차를 구합니다. gradient 계산은 손실을 줄이는 방향을 알려주고, 마지막으로 weights를 업데이트합니다.
실제 현업에서는 이 과정을 수천, 수만 번 반복합니다. 한 번의 업데이트로는 충분하지 않기 때문입니다.
마치 농구 선수가 하루에 수백 번 자유투를 연습하는 것처럼, 신경망도 반복 훈련을 통해 점점 정확해집니다. 주의할 점도 있습니다.
학습률을 너무 크게 설정하면 과녁을 지나쳐버리고, 너무 작게 설정하면 학습이 너무 느려집니다. 적절한 학습률을 찾는 것이 딥러닝 엔지니어의 중요한 역할 중 하나입니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.
"아, model.fit() 안에서 이런 일들이 반복되는 거군요!"
실전 팁
💡 - 학습률은 보통 0.001에서 시작해서 조절하는 것이 좋습니다
- 손실값이 줄어들지 않으면 학습률을 낮춰보세요
2. 경사 하강법
김개발 씨가 손실 함수 그래프를 보며 고민에 빠졌습니다. "손실을 최소화해야 한다는 건 알겠는데, 어떻게 최솟값을 찾죠?" 박시니어 씨가 웃으며 대답했습니다.
"산에서 내려올 때 어떻게 해? 가장 가파른 방향으로 한 걸음씩 내려가는 거지."
경사 하강법은 손실 함수의 최솟값을 찾아가는 최적화 알고리즘입니다. 마치 안개 낀 산에서 가장 낮은 골짜기를 찾아 내려가는 것과 같습니다.
현재 위치에서 가장 가파르게 내려가는 방향을 찾아 조금씩 이동하면, 결국 최솟값에 도달할 수 있습니다.
다음 코드를 살펴봅시다.
import numpy as np
def gradient_descent(X, y, learning_rate=0.01, epochs=1000):
# 가중치를 랜덤하게 초기화
weights = np.random.randn(X.shape[1])
losses = []
for epoch in range(epochs):
# 예측값 계산
predictions = np.dot(X, weights)
# 손실 계산 (MSE)
loss = np.mean((predictions - y) ** 2)
losses.append(loss)
# 기울기 계산: 손실 함수를 가중치로 미분
gradient = (2 / len(y)) * np.dot(X.T, (predictions - y))
# 기울기의 반대 방향으로 가중치 업데이트
weights = weights - learning_rate * gradient
return weights, losses
김개발 씨는 손실 함수라는 개념을 막 이해했습니다. 예측이 틀릴수록 손실값이 커지고, 맞을수록 작아진다는 것까지는 알겠습니다.
그런데 문제는 이 손실을 어떻게 줄이느냐입니다. 박시니어 씨가 화이트보드에 산 모양의 그래프를 그렸습니다.
"이게 손실 함수야. 우리의 목표는 이 산의 가장 낮은 골짜기를 찾는 거지." 경사 하강법이란 정확히 무엇일까요?
쉽게 비유하자면, 경사 하강법은 마치 짙은 안개 속에서 산을 내려오는 것과 같습니다. 앞이 잘 보이지 않으니 발밑의 경사만 느끼면서 가장 가파르게 내려가는 방향으로 한 걸음씩 옮깁니다.
그러다 보면 어느새 골짜기에 도착해 있습니다. 여기서 핵심 개념이 바로 기울기입니다.
수학적으로는 미분을 통해 계산하는데, 간단히 말하면 "현재 위치에서 어느 방향이 가장 가파른가"를 알려주는 값입니다. 기울기가 양수면 오르막이고, 음수면 내리막입니다.
경사 하강법의 동작 원리를 단계별로 살펴보겠습니다. 먼저 어딘가에서 시작합니다.
처음 가중치는 보통 랜덤하게 설정하는데, 이는 산 어딘가에 눈을 가리고 떨어뜨려지는 것과 같습니다. 다음으로 현재 위치의 기울기를 계산합니다.
코드에서 gradient 변수가 바로 이 값입니다. 이 기울기가 어느 방향으로 가야 손실이 줄어드는지 알려줍니다.
그리고 기울기의 반대 방향으로 이동합니다. 왜 반대일까요?
기울기가 양수라는 것은 그 방향으로 가면 손실이 증가한다는 뜻이니까요. 우리는 손실을 줄이고 싶으니 반대로 가야 합니다.
코드를 자세히 보면, weights = weights - learning_rate * gradient 부분이 핵심입니다. 기울기에 학습률을 곱한 만큼 빼주는 것이죠.
학습률이 너무 크면 골짜기를 지나쳐버리고, 너무 작으면 도착하는 데 너무 오래 걸립니다. 실무에서는 다양한 변형된 경사 하강법을 사용합니다.
SGD, Adam, RMSprop 같은 이름을 들어보셨을 겁니다. 이들은 모두 기본 경사 하강법을 개선한 것으로, 더 빠르고 안정적으로 최솟값을 찾습니다.
주의해야 할 점은 지역 최솟값에 빠질 수 있다는 것입니다. 산에 골짜기가 여러 개 있다면, 우리가 도착한 곳이 가장 낮은 곳이 아닐 수도 있습니다.
다행히 신경망의 손실 함수는 대부분의 지역 최솟값도 꽤 좋은 성능을 보입니다. 김개발 씨가 물었습니다.
"그럼 에포크를 많이 돌리면 무조건 좋은 건가요?" 박시니어 씨가 고개를 저었습니다. "아니, 너무 많이 돌리면 과적합이 생겨.
훈련 데이터만 너무 잘 맞추게 되는 거지."
실전 팁
💡 - Adam 옵티마이저가 대부분의 경우에 좋은 시작점입니다
- 학습률 스케줄링을 사용하면 처음엔 크게, 나중엔 작게 조절할 수 있습니다
3. 역전파
김개발 씨가 궁금해졌습니다. "기울기를 계산한다고 하셨는데, 신경망 층이 10개면 어떻게 해요?
각 층의 가중치를 일일이 미분해야 하나요?" 박시니어 씨가 미소를 지었습니다. "바로 그게 역전파의 힘이야.
연쇄 법칙 덕분에 뒤에서부터 차례로 계산하면 돼."
역전파는 출력층에서 시작해서 입력층 방향으로 기울기를 전파하는 알고리즘입니다. 마치 도미노가 거꾸로 쓰러지는 것처럼, 출력의 오차가 각 층으로 전달되면서 모든 가중치의 기울기를 효율적으로 계산할 수 있습니다.
다음 코드를 살펴봅시다.
import numpy as np
def backward_pass(X, y_true, y_pred, hidden_output, weights_hidden, weights_output):
# 출력층 오차 계산
output_error = y_pred - y_true
# 출력층 가중치의 기울기
d_weights_output = np.dot(hidden_output.T, output_error)
# 은닉층으로 오차 역전파 (연쇄 법칙 적용)
hidden_error = np.dot(output_error, weights_output.T)
# 활성화 함수의 미분 적용 (ReLU의 경우)
hidden_error *= (hidden_output > 0) # ReLU 미분: x > 0이면 1, 아니면 0
# 은닉층 가중치의 기울기
d_weights_hidden = np.dot(X.T, hidden_error)
return d_weights_hidden, d_weights_output
김개발 씨는 경사 하강법까지는 이해했습니다. 기울기 방향으로 가중치를 조정하면 된다는 것까지는 알겠는데, 문제가 있습니다.
신경망에는 수백만 개의 가중치가 있습니다. 이걸 어떻게 다 계산하죠?
박시니어 씨가 설명을 시작했습니다. "1986년에 힌튼 교수님이 역전파 알고리즘을 효과적으로 적용하는 방법을 발표하면서 딥러닝의 시대가 열렸어." 역전파란 정확히 무엇일까요?
쉽게 비유하자면, 역전파는 마치 실수의 원인을 추적하는 것과 같습니다. 최종 결과물에 문제가 있으면, "이 문제는 마지막 단계 때문인가?
아니면 그 전 단계? 아니면 더 전?"이라고 거슬러 올라가면서 각 단계의 책임을 따지는 것입니다.
역전파의 핵심은 연쇄 법칙입니다. 고등학교 수학에서 배운 합성함수의 미분 법칙이 바로 이것입니다.
f(g(x))를 미분하려면 f의 미분에 g의 미분을 곱하면 됩니다. 신경망의 각 층도 결국 함수의 합성이니까, 이 법칙을 반복 적용하면 됩니다.
순전파에서는 입력이 출력 방향으로 흘러갔습니다. 역전파에서는 반대로, 출력에서 계산된 오차가 입력 방향으로 흘러갑니다.
그래서 이름이 역전파, 영어로는 Backpropagation입니다. 코드를 살펴보겠습니다.
먼저 output_error에서 최종 오차를 계산합니다. 그 다음 이 오차를 이용해 출력층 가중치의 기울기 d_weights_output을 구합니다.
여기서 연쇄 법칙이 적용됩니다. 중요한 부분은 hidden_error 계산입니다.
출력층의 오차를 가중치와 곱해서 은닉층으로 전달합니다. 마치 최종 책임을 각 중간 단계에 분배하는 것과 같습니다.
hidden_error *= (hidden_output > 0) 부분은 활성화 함수의 미분입니다. ReLU의 경우 입력이 양수면 기울기가 1, 음수면 0입니다.
이렇게 각 층의 활성화 함수에 맞는 미분을 적용해야 합니다. 실무에서는 역전파를 직접 구현할 일이 거의 없습니다.
PyTorch나 TensorFlow 같은 프레임워크가 자동 미분 기능을 제공하기 때문입니다. 하지만 원리를 이해하면 학습이 안 될 때 어디가 문제인지 진단할 수 있습니다.
주의할 점은 기울기 소실 문제입니다. 층이 깊어질수록 기울기가 점점 작아져서 앞쪽 층은 거의 학습이 안 되는 현상입니다.
이 문제를 해결하기 위해 ReLU 같은 활성화 함수와 배치 정규화 같은 기법이 등장했습니다. 김개발 씨가 감탄했습니다.
"연쇄 법칙 하나로 이렇게 복잡한 문제를 푸는군요!" 박시니어 씨가 덧붙였습니다. "그래서 딥러닝에서 미적분이 중요한 거야.
코드로는 한 줄이지만, 그 안에 수학이 녹아있지."
실전 팁
💡 - 기울기 소실이 의심되면 각 층의 기울기 크기를 출력해보세요
- PyTorch의 .backward()가 바로 역전파를 수행하는 함수입니다
4. 시그모이드와 ReLU
김개발 씨가 신경망 구조를 보다가 이상한 점을 발견했습니다. "왜 층 사이에 이런 함수들을 넣는 거죠?
그냥 가중치만 곱하면 안 되나요?" 박시니어 씨가 종이에 직선을 그리며 말했습니다. "활성화 함수가 없으면 아무리 층을 쌓아도 결국 하나의 직선밖에 표현 못 해."
활성화 함수는 신경망에 비선형성을 부여하는 함수입니다. 시그모이드는 출력을 0과 1 사이로 압축하고, ReLU는 음수를 0으로, 양수는 그대로 통과시킵니다.
이 비선형성 덕분에 신경망은 복잡한 패턴을 학습할 수 있습니다.
다음 코드를 살펴봅시다.
import numpy as np
def sigmoid(x):
# 출력을 0~1 사이로 압축
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
# 시그모이드의 미분: 기울기 계산에 사용
s = sigmoid(x)
return s * (1 - s)
def relu(x):
# 음수는 0, 양수는 그대로 (간단하고 효율적)
return np.maximum(0, x)
def relu_derivative(x):
# ReLU의 미분: 양수면 1, 음수면 0
return (x > 0).astype(float)
# 사용 예시
x = np.array([-2, -1, 0, 1, 2])
print(f"Sigmoid: {sigmoid(x)}") # [0.12, 0.27, 0.5, 0.73, 0.88]
print(f"ReLU: {relu(x)}") # [0, 0, 0, 1, 2]
김개발 씨는 기본적인 신경망 구조를 이해했습니다. 입력에 가중치를 곱하고, 편향을 더하고, 다음 층으로 전달하고.
그런데 코드를 보니 층과 층 사이에 항상 무언가가 끼어 있습니다. 바로 활성화 함수입니다.
"왜 이게 필요한 거죠?" 김개발 씨의 질문에 박시니어 씨가 답했습니다. "활성화 함수가 없으면 신경망은 아무리 깊어도 선형 변환밖에 못 해.
직선으로는 곡선 데이터를 표현할 수 없잖아." 활성화 함수란 정확히 무엇일까요? 쉽게 비유하자면, 활성화 함수는 마치 문지기와 같습니다.
어떤 신호는 통과시키고, 어떤 신호는 막거나 변형합니다. 이 과정을 통해 신경망은 복잡한 결정 경계를 만들 수 있습니다.
먼저 시그모이드 함수를 살펴보겠습니다. 시그모이드는 어떤 값이든 0과 1 사이로 압축합니다.
매우 큰 양수는 1에 가깝게, 매우 작은 음수는 0에 가깝게 변환됩니다. 마치 확률처럼 해석할 수 있어서, 이진 분류의 출력층에 많이 사용됩니다.
하지만 시그모이드에는 치명적인 단점이 있습니다. 바로 기울기 소실 문제입니다.
시그모이드의 미분값을 보면, 최대가 0.25입니다. 층을 거칠 때마다 0.25를 계속 곱하면 어떻게 될까요?
기울기가 급격히 작아져서 앞쪽 층은 거의 학습이 되지 않습니다. 이 문제를 해결하기 위해 등장한 것이 ReLU입니다.
ReLU는 놀라울 정도로 단순합니다. 양수면 그대로 통과, 음수면 0.
끝입니다. 코드로는 np.maximum(0, x) 한 줄입니다.
이렇게 단순한데 왜 효과적일까요? 첫째, 양수 영역에서 기울기가 1입니다.
아무리 깊은 층을 통과해도 기울기가 사라지지 않습니다. 둘째, 계산이 빠릅니다.
지수 함수를 쓰는 시그모이드와 달리 비교 연산 하나면 됩니다. 셋째, 희소성을 만듭니다.
음수 입력에 대해 출력이 0이므로, 일부 뉴런만 활성화됩니다. 코드를 살펴보면, relu_derivative에서 (x > 0).astype(float)로 미분을 계산합니다.
양수면 1, 음수면 0이라는 뜻입니다. 시그모이드처럼 복잡한 미분 공식이 필요 없습니다.
실무에서는 은닉층에 ReLU를, 출력층에는 문제에 맞는 함수를 사용합니다. 이진 분류면 시그모이드, 다중 분류면 소프트맥스, 회귀면 선형 함수를 씁니다.
주의할 점은 ReLU의 죽은 뉴런 문제입니다. 한번 음수 영역에 빠진 뉴런은 기울기가 0이라 다시는 활성화되지 않을 수 있습니다.
이를 해결하기 위해 Leaky ReLU 같은 변형이 등장했습니다. 김개발 씨가 정리했습니다.
"결국 시그모이드는 출력층에, ReLU는 은닉층에 쓰면 되는 거군요!" 박시니어 씨가 고개를 끄덕였습니다. "대부분의 경우 그렇지.
하지만 상황에 따라 다른 선택이 필요할 때도 있어."
실전 팁
💡 - 은닉층에서는 ReLU를 기본으로 사용하세요
- 시그모이드는 이진 분류의 출력층에 적합합니다
5. tanh와 Leaky ReLU
김개발 씨가 다양한 활성화 함수를 공부하다가 질문했습니다. "ReLU가 좋다면서요?
그런데 왜 tanh나 Leaky ReLU 같은 다른 함수들도 있는 거예요?" 박시니어 씨가 답했습니다. "ReLU도 완벽하진 않아.
상황에 따라 더 나은 선택이 있을 수 있지."
tanh는 출력을 -1과 1 사이로 압축하여 시그모이드보다 중심이 0에 가깝습니다. Leaky ReLU는 음수 영역에서도 작은 기울기를 유지하여 ReLU의 죽은 뉴런 문제를 해결합니다.
두 함수 모두 특정 상황에서 ReLU보다 나은 성능을 보일 수 있습니다.
다음 코드를 살펴봅시다.
import numpy as np
def tanh(x):
# 출력을 -1~1 사이로 압축 (0 중심)
return np.tanh(x)
def tanh_derivative(x):
# tanh의 미분: 1 - tanh(x)^2
return 1 - np.tanh(x) ** 2
def leaky_relu(x, alpha=0.01):
# 음수 영역에서도 작은 기울기(alpha) 유지
return np.where(x > 0, x, alpha * x)
def leaky_relu_derivative(x, alpha=0.01):
# 양수면 1, 음수면 alpha
return np.where(x > 0, 1, alpha)
# 비교 예시
x = np.array([-2, -1, 0, 1, 2])
print(f"tanh: {tanh(x)}") # [-0.96, -0.76, 0, 0.76, 0.96]
print(f"Leaky ReLU: {leaky_relu(x)}") # [-0.02, -0.01, 0, 1, 2]
김개발 씨는 시그모이드와 ReLU를 배웠습니다. ReLU가 기울기 소실 문제를 해결했다는 것도 알았습니다.
그런데 논문을 읽다 보니 tanh나 Leaky ReLU를 쓰는 경우도 많았습니다. 왜일까요?
박시니어 씨가 먼저 tanh에 대해 설명했습니다. "시그모이드의 개선판이라고 생각하면 돼." tanh는 하이퍼볼릭 탄젠트의 줄임말입니다.
시그모이드가 01 사이로 압축한다면, tanh는 **-11 사이로 압축**합니다. 큰 차이가 없어 보이지만, 중요한 특성이 있습니다.
바로 0 중심이라는 점입니다. 시그모이드의 출력은 항상 양수입니다.
이게 왜 문제일까요? 다음 층의 가중치를 업데이트할 때, 기울기가 모두 같은 부호를 가지게 됩니다.
이러면 최적화 경로가 지그재그로 비효율적이 됩니다. tanh는 출력이 양수일 수도, 음수일 수도 있어서 이 문제가 줄어듭니다.
하지만 tanh도 여전히 기울기 소실 문제가 있습니다. 양 끝으로 갈수록 기울기가 0에 가까워지기 때문입니다.
그래서 깊은 네트워크에서는 ReLU 계열이 더 선호됩니다. 이제 Leaky ReLU를 살펴보겠습니다.
ReLU의 가장 큰 문제점을 기억하시나요? 바로 죽은 뉴런입니다.
입력이 음수면 출력도 0, 기울기도 0입니다. 한번 음수 영역에 빠지면 그 뉴런은 영원히 업데이트되지 않을 수 있습니다.
Leaky ReLU는 이 문제를 우아하게 해결합니다. 음수 영역에서 출력을 0으로 만드는 대신, 작은 기울기(보통 0.01)를 줍니다.
코드에서 alpha * x 부분이 바로 이것입니다. 코드를 보면, np.where(x > 0, x, alpha * x)로 구현됩니다.
x가 양수면 그대로, 음수면 0.01을 곱합니다. 미분도 마찬가지로 양수면 1, 음수면 0.01입니다.
이 작은 기울기 덕분에 뉴런이 완전히 죽지 않습니다. Leaky ReLU의 변형으로 PReLU가 있습니다.
alpha 값을 고정하지 않고 학습 가능한 파라미터로 만든 것입니다. 데이터에 따라 최적의 기울기를 스스로 찾습니다.
또 다른 변형인 ELU는 음수 영역에서 지수 함수를 사용합니다. 출력이 0 중심에 가까워지는 장점이 있지만, 지수 계산이 들어가서 ReLU보다 느립니다.
실무에서는 어떤 것을 선택해야 할까요? RNN이나 LSTM에서는 tanh가 여전히 많이 쓰입니다.
게이트 메커니즘과 잘 맞기 때문입니다. 일반적인 CNN이나 MLP에서는 ReLU나 Leaky ReLU가 표준입니다.
최근에는 GELU나 Swish 같은 새로운 함수들도 인기를 얻고 있습니다. 김개발 씨가 정리했습니다.
"결국 은행 처리를 만병통치약은 없고, 상황에 맞게 선택해야 하는군요." 박시니어 씨가 웃었습니다. "그게 바로 딥러닝 엔지니어링의 묘미야."
실전 팁
💡 - 죽은 뉴런이 의심되면 Leaky ReLU로 바꿔보세요
- RNN 계열에서는 tanh가 여전히 좋은 선택입니다
6. 소프트맥스 함수
김개발 씨가 이미지 분류 모델을 만들고 있었습니다. 고양이, 개, 새 세 가지 클래스가 있는데, 출력층을 어떻게 설계해야 할지 고민이었습니다.
"시그모이드를 세 개 쓰면 되나요?" 박시니어 씨가 고개를 저었습니다. "그러면 확률의 합이 1이 안 돼.
소프트맥스를 써야 해."
소프트맥스는 여러 개의 값을 합이 1이 되는 확률 분포로 변환합니다. 다중 분류 문제에서 각 클래스에 속할 확률을 계산할 때 사용됩니다.
가장 큰 값이 가장 높은 확률을 갖게 되어, 분류 결과를 확률적으로 해석할 수 있습니다.
다음 코드를 살펴봅시다.
import numpy as np
def softmax(x):
# 수치 안정성을 위해 최댓값을 빼줌
exp_x = np.exp(x - np.max(x))
return exp_x / np.sum(exp_x)
def softmax_batch(x):
# 배치 처리용: 각 샘플별로 소프트맥스 적용
exp_x = np.exp(x - np.max(x, axis=1, keepdims=True))
return exp_x / np.sum(exp_x, axis=1, keepdims=True)
# 사용 예시: 고양이, 개, 새 분류
logits = np.array([2.0, 1.0, 0.1])
probs = softmax(logits)
print(f"로짓: {logits}")
print(f"확률: {probs}") # [0.659, 0.242, 0.099]
print(f"합계: {np.sum(probs)}") # 1.0
classes = ['고양이', '개', '새']
print(f"예측: {classes[np.argmax(probs)]}") # 고양이
김개발 씨는 이미지 분류 프로젝트를 진행 중입니다. 입력 이미지가 고양이인지, 개인지, 새인지 구분해야 합니다.
신경망의 마지막 층에서 세 개의 숫자가 나오는데, 이걸 어떻게 해석해야 할까요? 처음에 김개발 씨는 각각에 시그모이드를 적용하려고 했습니다.
하지만 그러면 문제가 생깁니다. 세 확률의 합이 1이 되지 않습니다.
고양이 0.8, 개 0.7, 새 0.3이 나올 수 있는 겁니다. 이건 확률로 해석하기 어렵습니다.
소프트맥스가 바로 이 문제를 해결합니다. 쉽게 비유하자면, 소프트맥스는 마치 파이 차트를 만드는 것과 같습니다.
세 개의 값이 주어지면, 이를 전체의 비율로 변환합니다. 큰 값은 큰 조각을, 작은 값은 작은 조각을 차지하고, 모든 조각을 합하면 정확히 100%가 됩니다.
소프트맥스의 수학적 원리를 살펴보겠습니다. 먼저 모든 값에 지수 함수를 적용합니다.
e^x를 계산하는 것이죠. 왜 지수 함수일까요?
첫째, 모든 값을 양수로 만들어 확률로 해석할 수 있게 합니다. 둘째, 값의 차이를 증폭시켜 가장 큰 값이 더 두드러지게 만듭니다.
그 다음 모든 지수값의 합으로 나눕니다. 이렇게 하면 모든 출력의 합이 정확히 1이 됩니다.
수식으로 보면 softmax(x_i) = exp(x_i) / sum(exp(x_j))입니다. 코드에서 주목할 부분이 있습니다.
x - np.max(x)를 먼저 계산하는데, 이것은 수치 안정성을 위한 것입니다. 지수 함수는 큰 값에서 오버플로우가 발생할 수 있습니다.
최댓값을 빼도 결과는 같지만, 계산 과정에서 숫자가 너무 커지는 것을 방지합니다. 예시 코드에서 로짓 [2.0, 1.0, 0.1]이 확률 [0.659, 0.242, 0.099]로 변환됩니다.
가장 큰 값 2.0이 가장 높은 확률 0.659를 갖습니다. 합은 정확히 1.0입니다.
실무에서 소프트맥스는 크로스 엔트로피 손실과 함께 사용됩니다. 예측 확률 분포와 실제 정답 분포의 차이를 측정하는 것이죠.
대부분의 딥러닝 프레임워크는 이 둘을 합친 CrossEntropyLoss 함수를 제공합니다. 주의할 점이 있습니다.
소프트맥스는 상대적인 크기만 반영합니다. [2, 1, 0]과 [200, 100, 0]은 같은 확률 분포를 만들지 않습니다.
후자는 첫 번째 클래스의 확률이 거의 1에 가까워집니다. 이를 온도 파라미터로 조절하기도 합니다.
김개발 씨가 코드를 실행해보고 감탄했습니다. "오, 정말 합이 딱 1이 되네요!
이제 어떤 클래스일 확률이 몇 퍼센트인지 말할 수 있겠어요." 박시니어 씨가 덧붙였습니다. "맞아.
그래서 다중 분류에서는 항상 소프트맥스를 쓰는 거야."
실전 팁
💡 - 다중 분류의 출력층에는 반드시 소프트맥스를 사용하세요
- PyTorch에서는 CrossEntropyLoss가 소프트맥스를 포함하므로 따로 적용하지 마세요
7. 활성화 함수 선택 기준
김개발 씨가 새 프로젝트를 시작하려는데 고민이 됩니다. "활성화 함수가 이렇게 많은데, 어떤 걸 써야 하죠?" 박시니어 씨가 체크리스트를 건네며 말했습니다.
"문제 유형과 네트워크 구조에 따라 기준이 있어. 하나씩 살펴보자."
활성화 함수 선택은 문제 유형, 네트워크 깊이, 출력 형태에 따라 달라집니다. 은닉층에서는 ReLU 계열이 기본이고, 출력층은 이진 분류면 시그모이드, 다중 분류면 소프트맥스, 회귀면 선형 함수를 사용합니다.
이 기준을 알면 대부분의 상황에 적절한 선택을 할 수 있습니다.
다음 코드를 살펴봅시다.
import torch.nn as nn
# 문제 유형별 출력층 활성화 함수 선택
class ClassificationModel(nn.Module):
def __init__(self, input_size, hidden_size, num_classes, problem_type):
super().__init__()
# 은닉층: ReLU가 기본
self.hidden = nn.Sequential(
nn.Linear(input_size, hidden_size),
nn.ReLU(), # 또는 nn.LeakyReLU(0.01)
nn.Linear(hidden_size, hidden_size),
nn.ReLU()
)
self.output = nn.Linear(hidden_size, num_classes)
self.problem_type = problem_type
def forward(self, x):
x = self.hidden(x)
x = self.output(x)
# 출력층: 문제 유형에 따라 선택
if self.problem_type == 'binary':
return torch.sigmoid(x) # 이진 분류
elif self.problem_type == 'multiclass':
return torch.softmax(x, dim=1) # 다중 분류
else: # regression
return x # 회귀: 활성화 없음
김개발 씨는 이제 다양한 활성화 함수를 알게 되었습니다. 시그모이드, tanh, ReLU, Leaky ReLU, 소프트맥스까지.
하지만 막상 프로젝트를 시작하려니 어떤 것을 선택해야 할지 막막합니다. 박시니어 씨가 화이트보드에 표를 그리며 설명을 시작했습니다.
"크게 두 가지를 생각하면 돼. 어디에 사용하는지, 그리고 어떤 문제를 푸는지." 먼저 위치에 따른 선택을 알아보겠습니다.
은닉층에서는 ReLU가 표준입니다. 계산이 빠르고, 기울기 소실 문제가 적고, 대부분의 경우 잘 작동합니다.
죽은 뉴런이 걱정된다면 Leaky ReLU를 써도 됩니다. 최신 트렌드를 따르고 싶다면 GELU나 Swish도 좋은 선택입니다.
출력층은 문제 유형에 따라 달라집니다. 이것이 두 번째 기준입니다.
이진 분류 문제라면 시그모이드를 씁니다. 스팸 메일 분류, 질병 유무 판단처럼 예/아니오로 답하는 문제입니다.
출력이 0~1 사이의 확률로 해석됩니다. 다중 분류 문제라면 소프트맥스를 씁니다.
손글씨 숫자 인식(0~9), 이미지 분류(고양이/개/새)처럼 여러 클래스 중 하나를 고르는 문제입니다. 모든 클래스 확률의 합이 1이 됩니다.
회귀 문제라면 활성화 함수를 쓰지 않습니다. 집값 예측, 온도 예측처럼 연속적인 값을 출력하는 문제입니다.
선형 함수, 즉 항등 함수를 사용합니다. 몇 가지 특수한 경우도 있습니다.
멀티레이블 분류는 하나의 샘플이 여러 클래스에 속할 수 있는 경우입니다. 영화가 동시에 액션이면서 SF일 수 있듯이요.
이때는 소프트맥스가 아닌 시그모이드를 각 클래스에 독립적으로 적용합니다. RNN/LSTM에서는 게이트 메커니즘 때문에 tanh와 시그모이드가 내부적으로 사용됩니다.
하지만 이는 라이브러리가 처리해주므로 직접 설정할 일은 거의 없습니다. 배치 정규화를 사용한다면 활성화 함수의 선택이 조금 달라질 수 있습니다.
배치 정규화가 입력을 정규화해주므로 tanh의 포화 문제가 줄어듭니다. 하지만 여전히 ReLU가 더 빠릅니다.
코드를 보면 실제 구현 방식을 알 수 있습니다. 은닉층에서는 nn.ReLU()를 일관되게 사용하고, 출력층에서는 문제 유형에 따라 분기합니다.
참고로 PyTorch의 CrossEntropyLoss는 소프트맥스를 포함하므로, 다중 분류에서 모델은 로짓만 출력하고 손실 함수에서 소프트맥스를 적용하는 것이 일반적입니다. 실무에서는 처음에 가장 단순한 선택으로 시작하는 것이 좋습니다.
은닉층 ReLU, 출력층은 문제에 맞게. 성능이 안 나오면 그때 Leaky ReLU나 다른 함수를 시도해봅니다.
김개발 씨가 체크리스트를 정리했습니다. "은닉층은 ReLU, 이진 분류는 시그모이드, 다중 분류는 소프트맥스, 회귀는 없음.
이것만 기억하면 되겠네요!" 박시니어 씨가 끄덕였습니다. "맞아.
90%의 상황은 이 규칙으로 해결돼."
실전 팁
💡 - 기본 선택에서 시작하고 문제가 있을 때만 변경하세요
- PyTorch CrossEntropyLoss 사용 시 모델 출력에 소프트맥스를 적용하지 마세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
vLLM 통합 완벽 가이드
대규모 언어 모델 추론을 획기적으로 가속화하는 vLLM의 설치부터 실전 서비스 구축까지 다룹니다. PagedAttention과 연속 배칭 기술로 GPU 메모리를 효율적으로 활용하는 방법을 배웁니다.
Web UI Demo 구축 완벽 가이드
Gradio를 활용하여 머신러닝 모델과 AI 서비스를 위한 웹 인터페이스를 구축하는 방법을 다룹니다. 코드 몇 줄만으로 전문적인 데모 페이지를 만들고 배포하는 과정을 초급자도 쉽게 따라할 수 있도록 설명합니다.
Sandboxing & Execution Control 완벽 가이드
AI 에이전트가 코드를 실행할 때 반드시 필요한 보안 기술인 샌드박싱과 실행 제어에 대해 알아봅니다. 격리된 환경에서 안전하게 코드를 실행하고, 악성 동작을 탐지하는 방법을 단계별로 설명합니다.
Voice Design then Clone 워크플로우 완벽 가이드
AI 음성 합성에서 일관된 캐릭터 음성을 만드는 Voice Design then Clone 워크플로우를 설명합니다. 참조 음성 생성부터 재사용 가능한 캐릭터 구축까지 실무 활용법을 다룹니다.
Tool Use 완벽 가이드 - Shell, Browser, DB 실전 활용
AI 에이전트가 외부 도구를 활용하여 셸 명령어 실행, 브라우저 자동화, 데이터베이스 접근 등을 수행하는 방법을 배웁니다. 실무에서 바로 적용할 수 있는 패턴과 베스트 프랙티스를 담았습니다.