다시 돌아오셨군요! 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)될 수 있습니다!
  • 첫 번째 레이어는 간단한 패턴을 학습합니다.
  • 두 번째 레이어는 이러한 패턴을 결합하여 더 복잡한 아이디어를 만듭니다.
  • 세 번째 및 이후 레이어는 점점 더 추상적인 이해를 구축합니다.

신경망의 가중치와 바이어스

  1. 네트워크가 현재 가중치를 사용하여 예측을 수행합니다.
  2. 예측을 정답(목표 출력)과 비교합니다.
  3. 얼마나 틀렸는지 계산합니다 — 이것이 오류 또는 "손실"입니다.
  4. 역전파를 사용하여 각 가중치가 오류에 어떻게 영향을 미쳤는지 계산합니다.
  5. 각 가중치는 다음 번에 오류를 줄이기 위해 약간 조정됩니다 — 이 단계는 기울기(도함수)와 학습률을 사용하여 수행됩니다.
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까지제로 중심 숨겨진 레이어숨겨진 레이어시그모이드보다 더 강한 기울기
ReLU0에서 ∞까지대부분의 딥 네트워크 및 CNN숨겨진 레이어빠르고 효율적이지만 음수에서 "죽을" 수 있음

도함수와 역전파: 네트워크가 실수로부터 학습하는 방법

  1. 포워드 패스: 정답을 추측합니다.
  2. 손실 계산: 그 추측이 얼마나 틀렸나요?
  3. 백워드 패스: 각 가중치가 그 오류에 어떻게 영향을 미쳤는지 알아냅니다.
  4. 가중치 업데이트: 미래의 실수를 줄이기 위해 가중치를 약간 조정합니다.

학습률과 에포크

  • 에포크 수를 늘리면 네트워크가 개선할 기회가 더 많아져 손실을 더 줄일 수 있습니다. 그러나 너무 많은 에포크는 과적합(overfitting)을 초래할 수 있으며, 모델이 훈련 데이터에 너무 맞춰져 새로운 입력에서 성능이 저하될 수 있습니다.
  • 에포크 수를 줄이면 훈련이 더 빨라지지만, 네트워크가 충분히 학습하지 못할 수 있어 성능이 저하될 수 있습니다.

신경망이 여러 가지를 배울 수 있을까요?

  • 숫자를 빼기
  • 이미지 분류
  • 시계열에서 미래 값 예측

마무리