ยินดีต้อนรับกลับ! ในตอนที่ 1 เราได้เห็นว่า neural network สามารถบวกเลขสองตัวได้อย่างไรโดยใช้โครงสร้าง feedforward พื้นฐาน ที่ท้ายบทความ เราได้แสดงให้คุณเห็นการทำงานของ C++ แบบเต็มรูปแบบที่รวมถึงการส่งข้อมูลไปข้างหน้า, ลูปการฝึก, การคำนวณความสูญเสีย, และการอัปเดตน้ำหนัก—ทั้งหมดในโค้ด.

นี่นำไปสู่คำถามถัดไป: neural network เรียนรู้ที่จะปรับน้ำหนักเหล่านั้นด้วยตัวเองผ่านการฝึกได้อย่างไร? นั่นคือสิ่งที่เราจะสำรวจที่นี่ในตอนที่ 2.

ก่อนที่เราจะไปต่อ นี่คือโค้ด C++ แบบเต็มจากตอนที่ 1 ที่ใช้ในการสร้าง neural network ง่ายๆ เพื่อเรียนรู้วิธีการบวกเลขสองตัว มันรวมถึงการส่งข้อมูลไปข้างหน้า, การคำนวณความสูญเสีย, backpropagation, และการอัปเดตน้ำหนัก:

#include <iostream>
#include <cmath>
using namespace std;

// ฟังก์ชันการเปิดใช้งาน sigmoid
float sigmoid(float x) {
    return 1.0f / (1.0f + exp(-x));
}

// อนุพันธ์ของ sigmoid (ใช้ใน backpropagation)
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; // ความผิดพลาดที่ยกกำลังสอง (ความสูญเสีย)

                // ===== Backpropagation - เลเยอร์เอาต์พุต =====
                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;       // เกรดสำหรับอคติเอาต์พุต

                // ===== Backpropagation - เลเยอร์ที่ซ่อน =====
                // คำนวณว่าแต่ละโหนดที่ซ่อนมีส่วนช่วยต่อความผิดพลาดอย่างไร
                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; // เกรดสำหรับ w1 ของโหนด 1
                float grad_w2_node1 = delta_h1 * x2; // เกรดสำหรับ w2 ของโหนด 1
                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 epochs
        if ((epoch + 1) % 100 == 0 || epoch == 0)
            cout << "[สรุป] Epoch " << epoch + 1 << ": Loss = " << total_loss << endl;
    }

    // ===== ทดสอบโมเดลด้วยข้อมูลจากผู้ใช้ =====
    float a, b;
    cout << "\nทดสอบการคาดการณ์ผลรวม (a + b)\nกรุณาใส่ a: ";
    cin >> a;
    cout << "กรุณาใส่ 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;
}

ในส่วนนี้ เราจะอธิบายว่า neural network เรียนรู้ได้อย่างไร: ตั้งแต่การตั้งค่าเลเยอร์และฟังก์ชันการเปิดใช้งานไปจนถึงการปรับน้ำหนักผ่าน backpropagation คุณจะได้เรียนรู้ว่าเราฝึกโมเดลอย่างไรโดยใช้ learning rates และ epochs—และสำรวจว่าเครือข่ายสามารถทำงานที่ซับซ้อนกว่าการบวกเลขได้หรือไม่.

  • โหนดและเลเยอร์ทำงานอย่างไรใน neural network.
  • เรากำหนดน้ำหนักเริ่มต้นอย่างไรและปรับมันระหว่างการฝึก.
  • บทบาทของ learning rate และ epochs ในการกำหนดกระบวนการเรียนรู้.
  • ฟังก์ชันการเปิดใช้งานคืออะไร, ทำงานอย่างไร, และเมื่อใดควรใช้ sigmoid, tanh, หรือ ReLU.
  • วิธีที่ backpropagation และอนุพันธ์ช่วยให้เครือข่ายเรียนรู้จากความผิดพลาด.
  • เครือข่าย neural สามารถทำงานที่ซับซ้อนกว่าการบวกเลขได้หรือไม่.

พร้อมไหม? ไปกันเถอะ!

โหนดและเลเยอร์: เกิดอะไรขึ้นจริงๆ?

นิวรอน (หรือโหนด) เป็นบล็อกพื้นฐานของ neural network แต่ละนิวรอนรับค่าข้อมูลนำเข้า คูณด้วยน้ำหนัก เพิ่มอคติ และส่งผลลัพธ์ผ่านฟังก์ชันการเปิดใช้งานเพื่อผลิตผลลัพธ์ มันเหมือนกับการตัดสินใจเล็กๆ ที่เปลี่ยนแปลงข้อมูลทีละขั้นตอน.

เลเยอร์คือการรวมกันของนิวรอนที่ทำงานในระดับเดียวกันของเครือข่าย ข้อมูลจะไหลจากเลเยอร์หนึ่งไปยังอีกเลเยอร์หนึ่ง ประเภทหลักๆ ของเลเยอร์มีดังนี้:

  • เลเยอร์นำเข้า: รับข้อมูลดิบ (เช่น ตัวเลขที่คุณต้องการบวก).
  • เลเยอร์ที่ซ่อน: ทำการแปลงข้อมูลจากการนำเข้า เรียนรู้รูปแบบผ่านการเชื่อมต่อที่มีน้ำหนัก.
  • เลเยอร์เอาต์พุต: สร้างการคาดการณ์หรือผลลัพธ์สุดท้าย.

นิวรอนแต่ละตัวในเลเยอร์เชื่อมต่อกับนิวรอนในเลเยอร์ถัดไป และการเชื่อมต่อแต่ละตัวมีน้ำหนักที่เครือข่ายเรียนรู้และปรับระหว่างการฝึก.

คิดว่าแต่ละโหนด (นิวรอน) ใน neural network เป็นผู้ตัดสินใจเล็กๆ มันรับข้อมูลนำเข้า คูณด้วย "น้ำหนัก" ของมัน เพิ่ม "อคติ" แล้วใช้ฟังก์ชันการเปิดใช้งาน (เช่น sigmoid, tanh, หรือ ReLU).

โหนดมากขึ้น = พลังสมองมากขึ้น:

  • จำนวนโหนดที่น้อยอาจแก้ปัญหาง่ายๆ ได้อย่างรวดเร็ว.
  • โหนดมากขึ้นหมายความว่าเครือข่ายของคุณสามารถเข้าใจรูปแบบที่ซับซ้อนได้มากขึ้น แต่ถ้ามากเกินไปอาจทำให้มันช้าลงหรือทำให้มันคิดมากเกินไป (overfit)!

ถ้าเราซ้อนเลเยอร์? คุณสามารถซ้อนเลเยอร์ที่ซ่อนหลายๆ เลเยอร์ได้—นั่นคือ "deep neural network" แต่ละเลเยอร์เรียนรู้สิ่งที่นามธรรมมากขึ้นเล็กน้อย:

  • เลเยอร์แรกเรียนรู้รูปแบบง่ายๆ.
  • เลเยอร์ที่สองรวมรูปแบบเหล่านั้นเป็นแนวคิดที่ซับซ้อนมากขึ้น.
  • เลเยอร์ที่สามและเลเยอร์ถัดไปสร้างความเข้าใจที่นามธรรมมากขึ้นเรื่อยๆ.

น้ำหนักและอคติใน Neural Networks

น้ำหนักใน neural network คืออะไร?

ใน neural network, น้ำหนักคือค่าที่แสดงถึงความแข็งแกร่งของการเชื่อมต่อระหว่างนิวรอนสองตัว เมื่อข้อมูลนำไหลผ่านเครือข่าย มันจะถูกคูณด้วยน้ำหนักเหล่านี้ น้ำหนักที่สูงขึ้นหมายความว่าข้อมูลนำเข้ามีอิทธิพลมากขึ้นต่อผลลัพธ์ของนิวรอนถัดไป ระหว่างการฝึก น้ำหนักเหล่านี้จะถูกปรับเพื่อช่วยให้เครือข่ายทำการคาดการณ์ได้ดีขึ้น.

อคติใน neural network คืออะไร?

อคติคือพารามิเตอร์เพิ่มเติมใน neural network ที่ช่วยให้การเปิดใช้งานของนิวรอนสามารถเลื่อนไปได้ มันทำหน้าที่เหมือนการเลื่อนหรือเกณฑ์ที่ช่วยให้โมเดลปรับให้เข้ากับข้อมูลได้ดีขึ้นโดยช่วยให้มันเรียนรู้รูปแบบที่ไม่ผ่านจุดเริ่มต้น เช่นเดียวกับน้ำหนัก อคติจะถูกปรับระหว่างการฝึกเพื่อปรับปรุงการคาดการณ์.

เรากำหนดน้ำหนักเริ่มต้นอย่างไร?

เมื่อเราเริ่มการฝึก น้ำหนักจะต้องถูกกำหนดค่าเป็นบางค่า ในตัวอย่างโค้ดของเรา เราเพียงแค่ตั้งค่าแบบแมนนวลในตอนแรก:

float weight1_node1 = 0.5f, weight2_node1 = 0.5f, bias_node1 = 0.0f;

ในระบบที่ซับซ้อนมากขึ้น น้ำหนักมักจะถูกกำหนดค่าแบบสุ่มโดยใช้ค่าขนาดเล็ก (เช่น ระหว่าง -0.5 ถึง 0.5) ความสุ่มนี้ช่วยป้องกันความสมมาตร—ที่ซึ่งนิวรอนทั้งหมดเรียนรู้สิ่งเดียวกัน—และให้โอกาสเครือข่ายค้นพบรูปแบบที่มีประโยชน์ กลยุทธ์การกำหนดค่าที่ใช้บ่อย ได้แก่ Xavier (Glorot) และ He initialization ซึ่งออกแบบมาเพื่อรักษาสัญญาณที่เสถียรขณะที่มันไหลผ่านเครือข่าย สำหรับการทดลองง่ายๆ เช่นของเรา ค่าที่ตั้งค่าแบบแมนนวลหรือค่าขนาดเล็กแบบสุ่มก็เพียงพอแล้ว.

เราปรับน้ำหนักอย่างไร?

ระหว่างการฝึก นี่คือวิธีการปรับ:

  1. เครือข่ายทำการคาดการณ์โดยใช้น้ำหนักปัจจุบัน.
  2. มันเปรียบเทียบการคาดการณ์กับคำตอบที่ถูกต้อง (เป้าหมาย).
  3. มันคำนวณว่ามันผิดพลาดแค่ไหน — นี่คือความผิดพลาดหรือ "ความสูญเสีย".
  4. มันใช้ backpropagation เพื่อคำนวณว่าน้ำหนักแต่ละตัวมีผลต่อความผิดพลาดอย่างไร.
  5. น้ำหนักแต่ละตัวจะถูกปรับเล็กน้อยเพื่อลดความผิดพลาดในครั้งถัดไป — ขั้นตอนนี้ทำโดยใช้เกรด (อนุพันธ์) และ learning rate.

ในตัวอย่างโค้ด C++ ของเรา กระบวนการนี้เกิดขึ้นภายในลูปการฝึกที่ซ้อนกัน นี่คือการอ้างอิงอย่างรวดเร็ว:

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. Sigmoid

  • สูตร:   1 / (1 + e^(-x))
  • ช่วงผลลัพธ์: 0 ถึง 1
  • กรณีการใช้งาน: ยอดเยี่ยมสำหรับการจำแนกประเภทแบบไบนารีหรือเลเยอร์เอาต์พุตที่คุณต้องการค่าระหว่าง 0 และ 1.
  • ข้อดี: ผลลัพธ์ที่เรียบเนียนและมีขอบเขต.
  • ข้อเสีย: อิ่มตัวที่ขอบ (ใกล้ 0 หรือ 1), ทำให้การเรียนรู้ช้าลงเนื่องจากความชันที่หายไป.

2. Tanh

  • สูตร(e^x - e^(-x)) / (e^x + e^(-x))
  • ช่วงผลลัพธ์: -1 ถึง 1
  • กรณีการใช้งาน: เมื่อคุณต้องการผลลัพธ์ที่มีศูนย์กลางที่ 0.
  • ข้อดี: ความชันที่แข็งแกร่งกว่าซิกมอยด์; การเรียนรู้ที่เร็วขึ้นในเลเยอร์ที่ซ่อน.
  • ข้อเสีย: ยังคงประสบปัญหาความชันที่หายไปที่ขอบ.

3. ReLU (Rectified Linear Unit)

  • สูตร:   max(0, x)
  • ช่วงผลลัพธ์: 0 ถึงอนันต์.
  • กรณีการใช้งาน: ค่าเริ่มต้นสำหรับเลเยอร์ที่ซ่อนส่วนใหญ่ใน deep learning.
  • ข้อดี: การคำนวณที่รวดเร็ว ช่วยกับการเปิดใช้งานที่กระจาย.
  • ข้อเสีย: อาจตายหากข้อมูลนำเข้ามักเป็นค่าลบ ("dying ReLU problem").

เมื่อไหร่ควรใช้ฟังก์ชันการเปิดใช้งานใด?

ฟังก์ชันการเปิดใช้งานช่วงผลลัพธ์เหมาะสำหรับประเภทเลเยอร์ทั่วไปหมายเหตุ
Sigmoid0 ถึง 1ผลลัพธ์แบบไบนารีเลเยอร์เอาต์พุตอาจทำให้เกิดความชันที่หายไป
Tanh-1 ถึง 1เลเยอร์ที่ซ่อนที่มีศูนย์กลางที่ 0เลเยอร์ที่ซ่อนความชันที่แข็งแกร่งกว่าซิกมอยด์
ReLU0 ถึง ∞เครือข่ายลึกและ CNN ส่วนใหญ่เลเยอร์ที่ซ่อนรวดเร็วและมีประสิทธิภาพ แต่สามารถ "ตาย" ได้เมื่อเป็นค่าลบ

ในโค้ดตัวอย่างของเรา เราใช้ sigmoid สำหรับเลเยอร์ที่ซ่อน มันทำงานได้ดีสำหรับการสาธิตขนาดเล็ก แต่ในเครือข่ายที่ใหญ่ขึ้น ReLU มักจะถูกเลือกเพื่อประสิทธิภาพที่ดีกว่าและการฝึกที่เร็วขึ้น.

อนุพันธ์และ Backpropagation: วิธีที่เครือข่ายเรียนรู้จากความผิดพลาด

ลองนึกภาพว่าคุณถูกปิดตา ยืนอยู่บนเขา และต้องหาทางลงเขา คุณจะรู้สึกไปรอบๆ ด้วยเท้าเพื่อสัมผัสว่าทิศทางไหนลาดลง จากนั้นจึงเคลื่อนไปในทิศทางนั้น.

นั่นคือสิ่งที่ neural networks ทำโดยใช้อนุพันธ์—พวกเขาตรวจสอบว่าการปรับเล็กน้อยใดทำให้การคาดการณ์แม่นยำขึ้น.

หลังจากทำการคาดการณ์ เครือข่ายจะคำนวณว่ามันผิดพลาดแค่ไหน (เราจะเรียกสิ่งนี้ว่า "ความสูญเสีย") จากนั้นมันจะใช้สิ่งที่เรียกว่า backpropagation เพื่อติดตามความผิดพลาดนั้นย้อนกลับผ่านเครือข่าย โดยระบุว่าแต่ละน้ำหนักมีส่วนช่วยต่อความผิดพลาดอย่างไร.

นี่คือลูป backpropagation ที่เรียบง่าย:

  1. การส่งข้อมูลไปข้างหน้า: เดาคำตอบ.
  2. คำนวณความสูญเสีย: การเดานั้นผิดพลาดแค่ไหน?
  3. การส่งข้อมูลย้อนกลับ: ค้นหาว่าน้ำหนักแต่ละตัวมีผลต่อความผิดพลาดอย่างไร.
  4. อัปเดตน้ำหนัก: ปรับน้ำหนักเล็กน้อยเพื่อลดความผิดพลาดในอนาคต.

กระบวนการนี้จะทำซ้ำหลายพันครั้ง ทำให้เครือข่ายฉลาดขึ้นเรื่อยๆ.

มาดูการนำไปใช้ C++ ที่เรียบง่ายของแนวคิดนี้:

float sigmoid(float x) {
    return 1.0f / (1.0f + exp(-x));
}

float sigmoid_derivative(float y) {
    return y * (1 - y); // y = sigmoid(x)
}

นี่คือฟังก์ชันการเปิดใช้งานและอนุพันธ์ของมัน พวกมันถูกใช้ในทั้งการส่งข้อมูลไปข้างหน้าและย้อนกลับ.

ลูปการฝึกใช้ข้อมูลนำเข้า 2 ตัว (a และ b), ปรับให้เป็นระหว่าง 0 และ 1, และฝึกเครือข่ายเพื่อคาดการณ์ผลรวมของพวกมัน เราใช้ 3 โหนดที่ซ่อนและ 1 โหนดเอาต์พุต นี่คือส่วนสำคัญของลูป:

float error = target - y_pred;
total_loss += error * error;

float delta_out = error;
...
float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);

นี่คือที่ที่เราคำนวณความผิดพลาดและใช้อนุพันธ์เพื่อผลักดันการปรับกลับผ่านเครือข่าย—backpropagation ในทางปฏิบัติ.

ในตอนท้ายของการฝึก เราอนุญาตให้ผู้ใช้ป้อนตัวเลขสองตัว และเครือข่ายคาดการณ์ผลรวม มันไม่ได้จดจำ—มันกำลังทั่วไปจากสิ่งที่มันเรียนรู้.

อัตราการเรียนรู้และ Epochs

อย่างที่คุณอาจสังเกตในโค้ด เราใช้สองพารามิเตอร์ที่สำคัญในการควบคุมกระบวนการฝึก: learning_rate และ epochs.

อัตราการเรียนรู้คืออะไร?

อัตราการเรียนรู้กำหนดขนาดของแต่ละก้าวเมื่ออัปเดตน้ำหนัก อัตราการเรียนรู้ที่ต่ำ (เช่น 0.01) หมายความว่าเครือข่ายทำการปรับเล็กน้อยในแต่ละครั้ง ซึ่งปลอดภัยแต่ช้า อัตราการเรียนรู้ที่สูงอาจทำให้เร็วขึ้น แต่ถ้ามันสูงเกินไป เครือข่ายอาจพลาดและไม่สามารถเรียนรู้ได้อย่างถูกต้อง.

ในโค้ดของเรา เราใช้:

float learning_rate = 0.01f;

นี่ทำให้เรามีความสมดุลระหว่างความเร็วและความเสถียร.

Epoch คืออะไร?

Epoch คือ การวนซ้ำหนึ่งครั้ง ผ่านข้อมูลการฝึกทั้งหมด ในตัวอย่างของเรา สำหรับแต่ละ epoch เราฝึกเครือข่ายโดยใช้ทุกการรวมกันของ a และ b จาก 0 ถึง 99 จากนั้นเราจะทำอีกครั้งสำหรับ epoch ถัดไป.

เราฝึกซ้ำในหลายๆ epochs เพื่อให้เครือข่ายสามารถปรับน้ำหนักได้อีกครั้งและอีกครั้ง.

  • หากคุณเพิ่มจำนวน epochs เครือข่ายจะมีโอกาสปรับปรุงมากขึ้น ซึ่งอาจลดความสูญเสียลงได้มากขึ้น อย่างไรก็ตาม epochs ที่มากเกินไปอาจนำไปสู่ overfitting ซึ่งโมเดลจะถูกปรับให้เข้ากับข้อมูลการฝึกมากเกินไปและทำงานได้ไม่ดีในข้อมูลใหม่.
  • หากคุณลดจำนวน epochs การฝึกจะเร็วขึ้น แต่เครือข่ายอาจไม่เรียนรู้เพียงพอและอาจทำงานได้ไม่ดี.

ในโค้ด เราใช้:

int epochs = 1000;

นี่เป็นจุดเริ่มต้นที่ดีสำหรับปัญหาง่ายๆ เช่นการเรียนรู้การบวก แต่คุณสามารถปรับตามความเร็วหรือช้าของความสูญเสีย.

เครือข่าย neural สามารถเรียนรู้หลายสิ่งได้หรือไม่?

แน่นอนว่าได้ เครือข่าย neural ไม่จำกัดเพียงแค่การเรียนรู้ประเภทเดียวเท่านั้น ในความเป็นจริง สถาปัตยกรรมเครือข่ายเดียวกันสามารถฝึกให้ทำหลายสิ่งหลายอย่าง—เช่น การรู้จำตัวเลขที่เขียนด้วยมือ การแปลภาษา หรือแม้แต่การสร้างเพลง—ขึ้นอยู่กับข้อมูลที่มันได้รับและวิธีการฝึก.

ในตัวอย่างของเรา เครือข่ายเรียนรู้ที่จะบวกเลขสองตัว แต่ด้วยข้อมูลการฝึกที่แตกต่างกันและเลเยอร์เอาต์พุตที่ปรับเปลี่ยน มันก็สามารถเรียนรู้ที่จะ:

  • ลบตัวเลข
  • จำแนกรูปภาพ
  • คาดการณ์ค่าที่จะเกิดขึ้นในชุดข้อมูลเวลา

ยิ่งคุณต้องการให้เครือข่ายจัดการกับหลายงานมากขึ้น—หรือยิ่งงานซับซ้อนมากขึ้น—คุณอาจต้องการโหนดและเลเยอร์มากขึ้น ปัญหาง่ายๆ เช่นการบวกอาจต้องการโหนดเพียงไม่กี่ตัว ในขณะที่งานเช่นการรู้จำภาพหรือการแปลภาษาอาจต้องการเครือข่ายที่ลึกและกว้างมากขึ้น เพียงแค่ต้องจำไว้ว่า: ความซับซ้อนมากขึ้นไม่เสมอไปดีกว่า—การเพิ่มโหนดมากเกินไปอาจนำไปสู่ overfitting ซึ่งเครือข่ายจดจำแทนที่จะเรียนรู้ที่จะทั่วไป.

นี่คือเวอร์ชันที่ปรับปรุงของ neural network ของเรา ซึ่งเราฝึกให้ ทั้งบวกและลบ ตัวเลขนำเข้าด้วย เครือข่ายเดียวกัน:


#include <iostream>
#include <cmath>
using namespace std;

// ฟังก์ชันการเปิดใช้งาน sigmoid (บีบข้อมูลให้อยู่ในช่วง [0, 1])
float sigmoid(float x) {
    return 1.0f / (1.0f + exp(-x));
}

// อนุพันธ์ของ sigmoid (ใช้สำหรับคำนวณเกรดระหว่าง backpropagation)
float sigmoid_derivative(float y) {
    return y * (1 - y); // y = sigmoid(x)
}

int main() {
    // === น้ำหนักและอคติของเลเยอร์ที่ซ่อน (นิวรอนที่ซ่อน 3 ตัว, อินพุต 2 ตัว) ===
    float w1_n1 = 0.5f, w2_n1 = 0.5f, b1 = 0.0f;
    float w1_n2 = -0.5f, w2_n2 = -0.5f, b2 = 0.0f;
    float w1_n3 = 0.25f, w2_n3 = -0.25f, b3 = 0.0f;

    // === น้ำหนักและอคติของเลเยอร์เอาต์พุต (นิวรอนเอาต์พุต 2 ตัว: ผลรวมและลบ) ===
    float out_w1_sum = 0.5f, out_w2_sum = 0.5f, out_w3_sum = 0.5f, out_b_sum = 0.0f;
    float out_w1_sub = -0.5f, out_w2_sub = -0.5f, out_w3_sub = 0.5f, out_b_sub = 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_sum = (a + b) / 200.0f;          // ผลรวมอยู่ในช่วง 0 ถึง 198 → [0, ~1]
                float target_sub = (a - b + 100.0f) / 200.0f; // ผลลบอยู่ในช่วง -99 ถึง +99 → [0, ~1]

                // === การส่งข้อมูลไปข้างหน้า (อินพุต → เลเยอร์ที่ซ่อน) ===
                float z1 = x1 * w1_n1 + x2 * w2_n1 + b1;
                float h1 = sigmoid(z1);

                float z2 = x1 * w1_n2 + x2 * w2_n2 + b2;
                float h2 = sigmoid(z2);

                float z3 = x1 * w1_n3 + x2 * w2_n3 + b3;
                float h3 = sigmoid(z3);

                // === การส่งข้อมูลไปข้างหน้า (เลเยอร์ที่ซ่อน → เลเยอร์เอาต์พุต) ===
                float y_pred_sum = h1 * out_w1_sum + h2 * out_w2_sum + h3 * out_w3_sum + out_b_sum;
                float y_pred_sub = h1 * out_w1_sub + h2 * out_w2_sub + h3 * out_w3_sub + out_b_sub;

                // === คำนวณความสูญเสีย (ความผิดพลาดที่ยกกำลังสอง) ===
                float error_sum = target_sum - y_pred_sum;
                float error_sub = target_sub - y_pred_sub;
                total_loss += error_sum * error_sum + error_sub * error_sub;

                // === Backpropagation - เลเยอร์เอาต์พุต (เกรดสำหรับแต่ละนิวรอนเอาต์พุต) ===
                float delta_out_sum = error_sum;
                float grad_out_w1_sum = delta_out_sum * h1;
                float grad_out_w2_sum = delta_out_sum * h2;
                float grad_out_w3_sum = delta_out_sum * h3;
                float grad_out_b_sum = delta_out_sum;

                float delta_out_sub = error_sub;
                float grad_out_w1_sub = delta_out_sub * h1;
                float grad_out_w2_sub = delta_out_sub * h2;
                float grad_out_w3_sub = delta_out_sub * h3;
                float grad_out_b_sub = delta_out_sub;

                // === Backpropagation - เลเยอร์ที่ซ่อน ===
                float delta_h1 = (delta_out_sum * out_w1_sum + delta_out_sub * out_w1_sub) * sigmoid_derivative(h1);
                float delta_h2 = (delta_out_sum * out_w2_sum + delta_out_sub * out_w2_sub) * sigmoid_derivative(h2);
                float delta_h3 = (delta_out_sum * out_w3_sum + delta_out_sub * out_w3_sub) * sigmoid_derivative(h3);

                float grad_w1_n1 = delta_h1 * x1;
                float grad_w2_n1 = delta_h1 * x2;
                float grad_b1 = delta_h1;

                float grad_w1_n2 = delta_h2 * x1;
                float grad_w2_n2 = delta_h2 * x2;
                float grad_b2 = delta_h2;

                float grad_w1_n3 = delta_h3 * x1;
                float grad_w2_n3 = delta_h3 * x2;
                float grad_b3 = delta_h3;

                // === อัปเดตน้ำหนักเอาต์พุต ===
                out_w1_sum += learning_rate * grad_out_w1_sum;
                out_w2_sum += learning_rate * grad_out_w2_sum;
                out_w3_sum += learning_rate * grad_out_w3_sum;
                out_b_sum += learning_rate * grad_out_b_sum;

                out_w1_sub += learning_rate * grad_out_w1_sub;
                out_w2_sub += learning_rate * grad_out_w2_sub;
                out_w3_sub += learning_rate * grad_out_w3_sub;
                out_b_sub += learning_rate * grad_out_b_sub;

                // === อัปเดตน้ำหนักที่ซ่อน ===
                w1_n1 += learning_rate * grad_w1_n1;
                w2_n1 += learning_rate * grad_w2_n1;
                b1 += learning_rate * grad_b1;

                w1_n2 += learning_rate * grad_w1_n2;
                w2_n2 += learning_rate * grad_w2_n2;
                b2 += learning_rate * grad_b2;

                w1_n3 += learning_rate * grad_w1_n3;
                w2_n3 += learning_rate * grad_w2_n3;
                b3 += learning_rate * grad_b3;
            }
        }

        // === แสดงความสูญเสียทุก 100 epochs ===
        if ((epoch + 1) % 100 == 0 || epoch == 0)
            cout << "[Epoch " << epoch + 1 << "] Loss: " << total_loss << endl;
    }

    // === ขั้นตอนการทดสอบ ===
    float a, b;
    cout << "\nการทดสอบการคาดการณ์\nกรุณาใส่ a: ";
    cin >> a;
    cout << "กรุณาใส่ b: ";
    cin >> b;

    float x1 = a / 100.0f;
    float x2 = b / 100.0f;

    // ส่งข้อมูลไปข้างหน้าอีกครั้งสำหรับข้อมูลจากผู้ใช้
    float h1 = sigmoid(x1 * w1_n1 + x2 * w2_n1 + b1);
    float h2 = sigmoid(x1 * w1_n2 + x2 * w2_n2 + b2);
    float h3 = sigmoid(x1 * w1_n3 + x2 * w2_n3 + b3);

    float y_sum = h1 * out_w1_sum + h2 * out_w2_sum + h3 * out_w3_sum + out_b_sum;
    float y_sub = h1 * out_w1_sub + h2 * out_w2_sub + h3 * out_w3_sub + out_b_sub;

    // ปรับค่าผลลัพธ์
    float predicted_sum = y_sum * 200.0f;
    float predicted_sub = y_sub * 200.0f - 100.0f;

    cout << "ผลรวมที่คาดการณ์:        " << predicted_sum << endl;
    cout << "ผลรวมจริง:           " << (a + b) << endl;
    cout << "ผลต่างที่คาดการณ์: " << predicted_sub << endl;
    cout << "ผลต่างจริง:    " << (a - b) << endl;

    return 0;
}

สรุป

นี่คือข้อมูลที่ต้องใช้ในการทำความเข้าใจ—แต่มันก็น่าทึ่งใช่ไหม? เราไม่ได้พูดถึงทฤษฎีเพียงอย่างเดียว; เราได้เห็นว่า neural network เรียนรู้ที่จะทำสิ่งที่เป็นจริง: การบวกและการลบตัวเลข ไม่มีการเขียนกฎตายตัว—แค่น้ำหนักที่ปรับตัวเองผ่านการฝึก ในระหว่างทาง เราได้รู้จักกับอัตราการเรียนรู้ ฟังก์ชันการเปิดใช้งาน และเวทมนตร์ของ backpropagation ที่เชื่อมโยงทุกอย่างเข้าด้วยกัน.

ในตอนถัดไป เราจะถอยกลับจากโค้ดและดำดิ่งสู่เรื่องราวเบื้องหลังทั้งหมดนี้ เครือข่าย neural มาจากไหน? ทำไม AI ถึงเติบโตอย่างรวดเร็วในช่วงไม่กี่ปีที่ผ่านมา? อะไรเปลี่ยนไป? เราจะสำรวจว่าทุกอย่างเริ่มต้นอย่างไร—และทำไมมันถึงเพิ่งเริ่มต้น.