다시 돌아오셨군요! 1부에서는 신경망이 기본적인 피드포워드 구조를 사용하여 두 숫자를 더하는 방법을 살펴보았습니다. 기사 끝에서는 포워드 패스, 훈련 루프, 손실 계산 및 가중치 업데이트를 포함한 전체 C++ 구현도 보여드렸습니다. 모두 코드로요.
이제 자연스럽게 다음 질문이 떠오르죠: 신경망은 훈련을 통해 가중치를 어떻게 스스로 조정할까요? 바로 그 점을 2부에서 탐구해 보겠습니다.
더 나아가기 전에, 1부에서 두 숫자를 더하는 방법을 배우기 위해 간단한 신경망을 구현한 전체 C++ 코드를 보여드리겠습니다. 이 코드는 포워드 패스, 손실 계산, 역전파 및 가중치 업데이트를 포함합니다:
#include <iostream>
#include <cmath>
using namespace std;
// 시그모이드 활성화 함수
float sigmoid(float x) {
return 1.0f / (1.0f + exp(-x));
}
// 시그모이드의 도함수 (역전파에서 사용)
float sigmoid_derivative(float y) {
return y * (1 - y); // y = sigmoid(x)
}
int main() {
// 3개의 숨겨진 노드에 대한 가중치와 바이어스 초기화
// 이 값들은 학습 과정을 더 명확하게 보여주기 위해 수동으로 선택되었습니다.
// 양수와 음수의 가중치를 모두 사용하면 대칭을 방지하고 다양한 시작점을 제공합니다.
float weight1_node1 = 0.5f, weight2_node1 = 0.5f, bias_node1 = 0.0f; // 노드 1은 두 입력 모두에서 긍정적인 영향을 받습니다.
float weight1_node2 = -0.5f, weight2_node2 = -0.5f, bias_node2 = 0.0f; // 노드 2는 부정적인 영향을 받아 균형을 장려합니다.
float weight1_node3 = 0.25f, weight2_node3 = -0.25f, bias_node3 = 0.0f; // 노드 3은 혼합되어 상호작용을 포착하는 데 유용합니다.
// 출력 레이어 가중치와 바이어스 초기화
float weight_out_node1 = 0.5f, weight_out_node2 = 0.5f, weight_out_node3 = 0.5f, bias_out = 0.0f;
float learning_rate = 0.01f;
int epochs = 1000; // 훈련 데이터를 반복하는 횟수
// 훈련 루프
for (int epoch = 0; epoch < epochs; ++epoch) {
float total_loss = 0.0f;
for (int a = 0; a < 100; ++a) {
for (int b = 0; b < 100; ++b) {
float x1 = a / 100.0f;
float x2 = b / 100.0f;
float target = (a + b) / 200.0f; // 목표 합계를 [0, 1]로 정규화
// ===== 포워드 패스 =====
float z1 = x1 * weight1_node1 + x2 * weight2_node1 + bias_node1;
float h1 = sigmoid(z1);
float z2 = x1 * weight1_node2 + x2 * weight2_node2 + bias_node2;
float h2 = sigmoid(z2);
float z3 = x1 * weight1_node3 + x2 * weight2_node3 + bias_node3;
float h3 = sigmoid(z3);
float y_pred = h1 * weight_out_node1 + h2 * weight_out_node2 + h3 * weight_out_node3 + bias_out;
float error = target - y_pred;
total_loss += error * error; // 제곱 오차 (손실)
// ===== 역전파 - 출력 레이어 =====
float delta_out = error; // y_pred에 대한 손실의 도함수
float grad_out_w1 = delta_out * h1; // 출력 가중치 1에 대한 기울기
float grad_out_w2 = delta_out * h2; // 출력 가중치 2에 대한 기울기
float grad_out_w3 = delta_out * h3; // 출력 가중치 3에 대한 기울기
float grad_out_b = delta_out; // 출력 바이어스에 대한 기울기
// ===== 역전파 - 숨겨진 레이어 =====
// 각 숨겨진 노드가 오류에 어떻게 기여했는지 계산
float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);
float delta_h2 = delta_out * weight_out_node2 * sigmoid_derivative(h2);
float delta_h3 = delta_out * weight_out_node3 * sigmoid_derivative(h3);
float grad_w1_node1 = delta_h1 * x1; // 노드1의 w1에 대한 기울기
float grad_w2_node1 = delta_h1 * x2; // 노드1의 w2에 대한 기울기
float grad_b_node1 = delta_h1; // 노드1의 바이어스에 대한 기울기
float grad_w1_node2 = delta_h2 * x1;
float grad_w2_node2 = delta_h2 * x2;
float grad_b_node2 = delta_h2;
float grad_w1_node3 = delta_h3 * x1;
float grad_w2_node3 = delta_h3 * x2;
float grad_b_node3 = delta_h3;
// ===== 출력 레이어 가중치 업데이트 =====
weight_out_node1 += learning_rate * grad_out_w1;
weight_out_node2 += learning_rate * grad_out_w2;
weight_out_node3 += learning_rate * grad_out_w3;
bias_out += learning_rate * grad_out_b;
// ===== 숨겨진 레이어 가중치 업데이트 =====
weight1_node1 += learning_rate * grad_w1_node1;
weight2_node1 += learning_rate * grad_w2_node1;
bias_node1 += learning_rate * grad_b_node1;
weight1_node2 += learning_rate * grad_w1_node2;
weight2_node2 += learning_rate * grad_w2_node2;
bias_node2 += learning_rate * grad_b_node2;
weight1_node3 += learning_rate * grad_w1_node3;
weight2_node3 += learning_rate * grad_w2_node3;
bias_node3 += learning_rate * grad_b_node3;
}
}
// 100 에포크마다 손실 로그
if ((epoch + 1) % 100 == 0 || epoch == 0)
cout << "[요약] 에포크 " << epoch + 1 << ": 손실 = " << total_loss << endl;
}
// ===== 사용자 입력으로 모델 테스트 =====
float a, b;
cout << "\n합 예측 테스트 (a + b)\nEnter a: ";
cin >> a;
cout << "Enter b: ";
cin >> b;
float x1 = a / 100.0f;
float x2 = b / 100.0f;
// 훈련된 가중치로 다시 포워드 패스
float h1 = sigmoid(x1 * weight1_node1 + x2 * weight2_node1 + bias_node1);
float h2 = sigmoid(x1 * weight1_node2 + x2 * weight2_node2 + bias_node2);
float h3 = sigmoid(x1 * weight1_node3 + x2 * weight2_node3 + bias_node3);
float y_pred = h1 * weight_out_node1 + h2 * weight_out_node2 + h3 * weight_out_node3 + bias_out;
float predicted_sum = y_pred * 200.0f;
// 결과 출력
cout << "예측된 합: " << predicted_sum << "\n실제 합: " << (a + b) << endl;
return 0;
}
이번 파트에서는 신경망이 어떻게 학습하는지, 레이어와 활성화 함수를 설정하는 것부터 시작해 역전파를 통해 가중치를 조정하는 방법까지 살펴보겠습니다. 또한 학습률과 에포크를 사용하여 모델을 훈련하는 방법을 배우고, 신경망이 덧셈과 같은 간단한 작업을 넘어설 수 있는지에 대해서도 탐구할 것입니다.
- 신경망에서 노드와 레이어가 실제로 하는 일.
- 초기 가중치를 설정하고 훈련 중에 조정하는 방법.
- 학습 과정에서 학습률과 에포크의 역할.
- 활성화 함수가 무엇인지, 어떻게 작동하는지, 시그모이드, tanh 또는 ReLU를 언제 사용하는지.
- 역전파와 도함수가 네트워크가 실수로부터 학습할 수 있도록 하는 방법.
- 신경망이 덧셈과 같은 간단한 작업을 넘어 여러 가지를 배울 수 있는지 여부.
노드와 레이어: 실제로 무슨 일이 일어나고 있나요?
신경망에서 뉴런(또는 노드)은 기본 빌딩 블록입니다. 각 뉴런은 입력 값을 받아 가중치로 곱하고, 바이어스를 더한 후, 활성화 함수를 통해 결과를 출력합니다. 마치 데이터를 단계별로 변환하는 작은 결정 메이커와 같습니다.
레이어는 네트워크의 동일한 수준에서 작동하는 뉴런의 모음입니다. 정보는 한 레이어에서 다음 레이어로 흐릅니다. 세 가지 주요 유형의 레이어는 다음과 같습니다:
- 입력 레이어: 원시 데이터를 수신합니다 (예: 더하고자 하는 숫자).
- 숨겨진 레이어: 입력에 대한 변환을 수행하며, 가중치 연결을 통해 패턴을 학습합니다.
- 출력 레이어: 최종 예측 또는 결과를 생성합니다.

- 소수의 노드로 간단한 문제를 빠르게 해결할 수 있습니다.
- 더 많은 노드는 네트워크가 더 복잡한 패턴을 이해할 수 있게 하지만, 너무 많으면 느려지거나 과적합(overfit)될 수 있습니다!
- 첫 번째 레이어는 간단한 패턴을 학습합니다.
- 두 번째 레이어는 이러한 패턴을 결합하여 더 복잡한 아이디어를 만듭니다.
- 세 번째 및 이후 레이어는 점점 더 추상적인 이해를 구축합니다.
신경망의 가중치와 바이어스
- 네트워크가 현재 가중치를 사용하여 예측을 수행합니다.
- 예측을 정답(목표 출력)과 비교합니다.
- 얼마나 틀렸는지 계산합니다 — 이것이 오류 또는 "손실"입니다.
- 역전파를 사용하여 각 가중치가 오류에 어떻게 영향을 미쳤는지 계산합니다.
- 각 가중치는 다음 번에 오류를 줄이기 위해 약간 조정됩니다 — 이 단계는 기울기(도함수)와 학습률을 사용하여 수행됩니다.
float error = target - y_pred;
float delta_out = error;
float grad_out_w1 = delta_out * h1;
float grad_out_w2 = delta_out * h2;
float grad_out_w3 = delta_out * h3;
float grad_out_b = delta_out;
float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);
float delta_h2 = delta_out * weight_out_node2 * sigmoid_derivative(h2);
float delta_h3 = delta_out * weight_out_node3 * sigmoid_derivative(h3);
float grad_w1_node1 = delta_h1 * x1;
float grad_w2_node1 = delta_h1 * x2;
float grad_b_node1 = delta_h1;
// 가중치 업데이트
weight_out_node1 += learning_rate * grad_out_w1;
weight_out_node2 += learning_rate * grad_out_w2;
weight_out_node3 += learning_rate * grad_out_w3;
bias_out += learning_rate * grad_out_b;
weight1_node1 += learning_rate * grad_w1_node1;
weight2_node1 += learning_rate * grad_w2_node1;
bias_node1 += learning_rate * grad_b_node1;
활성화 함수: 노드에 개성을 부여하기
- 공식:
1 / (1 + e^(-x))
- 출력 범위: 0에서 1까지
사용 사례: 이진 분류 또는 0과 1 사이의 값이 필요한 출력 레이어에 적합합니다.
- 장점: 부드럽고 제한된 출력.
- 단점: 극단에서 포화(0 또는 1 근처)되어 기울기 소실로 인해 학습이 느려질 수 있습니다.

- 공식:
(e^x - e^(-x)) / (e^x + e^(-x))
출력 범위: -1에서 1까지
- 사용 사례: 제로 중심 출력을 원할 때.
- 장점: 시그모이드보다 더 강한 기울기; 숨겨진 레이어에서 더 빠른 학습.
- 단점: 극단에서 여전히 기울기 소실 문제를 겪습니다.

- 공식:
max(0, x)
- 출력 범위: 0에서 무한대까지.
사용 사례: 딥러닝의 대부분 숨겨진 레이어의 기본값.
- 장점: 빠른 계산, 희소 활성화에 도움.
- 단점: 입력이 항상 음수일 경우 죽을 수 있습니다 ("죽은 ReLU 문제").

활성화 | 출력 범위 | 좋은 경우 | 일반적인 레이어 유형 | 비고 |
---|---|---|---|---|
시그모이드 | 0에서 1까지 | 이진 출력 | 출력 레이어 | 기울기 소실을 유발할 수 있음 |
tanh | -1에서 1까지 | 제로 중심 숨겨진 레이어 | 숨겨진 레이어 | 시그모이드보다 더 강한 기울기 |
ReLU | 0에서 ∞까지 | 대부분의 딥 네트워크 및 CNN | 숨겨진 레이어 | 빠르고 효율적이지만 음수에서 "죽을" 수 있음 |
도함수와 역전파: 네트워크가 실수로부터 학습하는 방법
- 포워드 패스: 정답을 추측합니다.
- 손실 계산: 그 추측이 얼마나 틀렸나요?
- 백워드 패스: 각 가중치가 그 오류에 어떻게 영향을 미쳤는지 알아냅니다.
- 가중치 업데이트: 미래의 실수를 줄이기 위해 가중치를 약간 조정합니다.
학습률과 에포크
- 에포크 수를 늘리면 네트워크가 개선할 기회가 더 많아져 손실을 더 줄일 수 있습니다. 그러나 너무 많은 에포크는 과적합(overfitting)을 초래할 수 있으며, 모델이 훈련 데이터에 너무 맞춰져 새로운 입력에서 성능이 저하될 수 있습니다.
- 에포크 수를 줄이면 훈련이 더 빨라지지만, 네트워크가 충분히 학습하지 못할 수 있어 성능이 저하될 수 있습니다.
신경망이 여러 가지를 배울 수 있을까요?
- 숫자를 빼기
- 이미지 분류
- 시계열에서 미래 값 예측