คณิตศาสตร์เบื้องหลัง AI: เริ่มต้นง่ายๆ

คุณจะเขียนโปรแกรมเพื่อบวกเลขสองจำนวนที่น้อยกว่า 100 ได้อย่างไร? ง่ายใช่ไหม? โดยปกติแล้วคุณจะเขียนอะไรที่ตรงไปตรงมาแบบนี้:

#include <iostream>
using namespace std;

int main() {
    int a, b, sum;

    cout << "กรุณาใส่หมายเลขแรก: ";
    cin >> a;

    cout << "กรุณาใส่หมายเลขที่สอง: ";
    cin >> b;

    sum = a + b;

    cout << "ผลรวมคือ: " << sum << endl;

    return 0;
}

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

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

นี่คือภาพรวมโครงสร้างภายในของ neural network ที่ได้รับการฝึกแล้ว:

// น้ำหนักและอคติของชั้นซ่อน
double weight1_node1 = 0.658136;
double weight2_node1 = 0.840666;
double bias_node1 = -0.893218;

double weight1_node2 = -0.720667;
double weight2_node2 = -0.369172;
double bias_node2 = 0.036762;

double weight1_node3 = 0.512252;
double weight2_node3 = 0.292342;
double bias_node3 = -0.0745917;

// น้ำหนักและอคติของชั้นผลลัพธ์
double weight_out_node1 = 1.93108;
double weight_out_node2 = -0.718584;
double weight_out_node3 = 0.589741;
double bias_out = -0.467899;

เมื่อมองแวบแรก ค่าต่างๆ เหล่านี้อาจดูสุ่ม แต่พวกมันแทนสมองเทียมเล็กๆ ที่สามารถเรียนรู้วิธีการบวกเลขสองจำนวนได้

ใช้สูตรนี้ในการคำนวณผลลัพธ์ อย่ากังวลหากมันดูซับซ้อนในตอนนี้—แค่ตามไปเรื่อยๆ:

ที่นี่, x1 และ x2 คือสองหมายเลขที่คุณต้องการบวก (ปรับขนาดโดยการหารด้วย 100):

double x1 = 0.3; // 30 / 100
double x2 = 0.5; // 50 / 100

// ชั้นซ่อน
double z1 = x1 * weight1_node1 + x2 * weight2_node1 + bias_node1;
double h1 = sigmoid(z1);

double z2 = x1 * weight1_node2 + x2 * weight2_node2 + bias_node2;
double h2 = sigmoid(z2);
double z3 = x1 * weight1_node3 + x2 * weight2_node3 + bias_node3;
double h3 = sigmoid(z3);

// ชั้นผลลัพธ์
double sum = (h1 * weight_out_node1 + h2 * weight_out_node2 + h3 * weight_out_node3 + bias_out) * 200.0f;

มาดูว่าทำไมเราถึงคูณด้วย 200: เราได้ปรับขนาดข้อมูลนำเข้าและผลลัพธ์ให้พอดีกับช่วง 0 ถึง 1 เพื่อการฝึกที่ง่ายขึ้น เนื่องจากผลรวมสูงสุดของเลขสองจำนวนที่น้อยกว่า 100 คือ 200 เราจึงปรับขนาดผลลัพธ์กลับขึ้นโดยการคูณด้วย 200

ฟังก์ชัน Sigmoid:

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

มาลองใช้กับตัวเลขจริงกันเถอะ:

เราจะใช้ x1 = 30 และ x2 = 50. เราจะปรับขนาดพวกมันก่อน:

x1 = 30 / 100 = 0.3
x2 = 50 / 100 = 0.5

ตอนนี้เรามาทำการคำนวณกัน:

z1 = (0.3 × 0.658136) + (0.5 × 0.840666) - 0.893218
   ≈ 0.1974408 + 0.420333 - 0.893218
   ≈ -0.2754442
h1 = sigmoid(z1) ≈ 0.431844

z2 = (0.3 × -0.720667) + (0.5 × -0.369172) + 0.036762
   ≈ -0.2162001 - 0.184586 + 0.036762
   ≈ -0.3640241
h2 = sigmoid(z2) ≈ 0.409872

z3 = (0.3 × 0.512252) + (0.5 × 0.292342) - 0.0745917
   ≈ 0.1536756 + 0.146171 - 0.0745917
   ≈ 0.2252549
h3 = sigmoid(z3) ≈ 0.556078

// ชั้นผลลัพธ์สุดท้าย
output = (h1 × 1.93108) + (h2 × -0.718584) + (h3 × 0.589741) - 0.467899
       ≈ (0.834389) + (-0.294320) + (0.328013) - 0.467899
       ≈ 0.400183

ผลรวมที่คาดการณ์ = 0.400183 × 200 ≈ 80.0366
// ทำไมถึงคูณด้วย 200? เพราะในระหว่างการฝึก เราได้ปรับขนาดข้อมูลนำเข้าและผลลัพธ์ทั้งหมดให้คงอยู่ระหว่าง 0 ถึง 1 เพื่อความเสถียรในการเรียนรู้ที่ดีกว่า เนื่องจากผลรวมสูงสุดของข้อมูลนำเข้าสองตัวคือ 100 + 100 = 200 เราจึงปรับขนาดผลลัพธ์ของเครือข่ายกลับขึ้นโดยการคูณด้วย 200.

Boom! ผลลัพธ์คือประมาณ 80 ซึ่งเป็นผลรวมที่ถูกต้องของ 30 และ 50.

ถ้าคุณไม่เชื่อว่าเครือข่ายเล็กๆ นี้สามารถบวกได้จริง ลองทดสอบอีกครั้งด้วยหมายเลขที่แตกต่างกัน: 20 และ 30.

เราจะปรับขนาดข้อมูลนำเข้าสำหรับหมายเลขเหล่านี้ก่อน:

x1 = 20 / 100 = 0.2
x2 = 30 / 100 = 0.3

จากนั้นเราจะทำการคำนวณ:

z1 = (0.2 × 0.658136) + (0.3 × 0.840666) - 0.893218
   ≈ 0.1316272 + 0.2521998 - 0.893218
   ≈ -0.509391
h1 = sigmoid(z1) ≈ 0.375241

z2 = (0.2 × -0.720667) + (0.3 × -0.369172) + 0.036762
   ≈ -0.1441334 - 0.1107516 + 0.036762
   ≈ -0.218123
h2 = sigmoid(z2) ≈ 0.445714

z3 = (0.2 × 0.512252) + (0.3 × 0.292342) - 0.0745917
   ≈ 0.1024504 + 0.0877026 - 0.0745917
   ≈ 0.1155613
h3 = sigmoid(z3) ≈ 0.528860

// ชั้นผลลัพธ์สุดท้าย
output = (h1 × 1.93108) + (h2 × -0.718584) + (h3 × 0.589741) - 0.467899
       ≈ (0.724688) + (-0.320095) + (0.311644) - 0.467899
       ≈ 0.248338

ผลรวมที่คาดการณ์ = 0.248338 × 200 ≈ 49.6676

มันคาดการณ์ประมาณ 49.67 ซึ่งใกล้เคียงกับผลรวมที่แท้จริง: 50!

เจ๋งใช่ไหม?

แล้ว... Neural Network คืออะไร?

บทนำอย่างรวดเร็ว: Neural network เป็นเหมือนเวอร์ชันทางคณิตศาสตร์ของสมองของคุณ มันประกอบด้วยหน่วยที่เรียกว่า neurons ที่ประมวลผลข้อมูลนำเข้าโดยการคูณด้วยน้ำหนัก เพิ่มอคติ และจากนั้นใช้ฟังก์ชันการกระตุ้น ฟังก์ชันการกระตุ้นที่ใช้กันทั่วไปคือ sigmoid:

sigmoid(x) = 1 / (1 + e^(-x))

นี่จะบีบข้อมูลนำเข้าสู่ค่าระหว่าง 0 และ 1 ช่วยให้เครือข่ายสามารถจับลวดลายที่ซับซ้อนได้

นี่คือกราฟของฟังก์ชัน sigmoid ที่แสดงให้เห็นว่าข้อมูลนำเข้าสามารถถูกแปลงเป็นค่าระหว่าง 0 และ 1 ได้อย่างราบรื่น:

ยังมีฟังก์ชันการกระตุ้นอื่นๆ เช่น ReLU (Rectified Linear Unit), tanh, และ softmax ซึ่งแต่ละตัวมีกรณีการใช้งานและพฤติกรรมของตัวเอง แต่ sigmoid เป็นจุดเริ่มต้นที่ดีเพราะมันเรียบง่ายและราบรื่น—เหมาะสำหรับ neural networks ขนาดเล็กอย่างของเรา

การตั้งค่า: สอนเครือข่ายของเราให้บวก

สำหรับการทดลองนี้ เราได้สร้างเครือข่ายขนาดเล็กที่มี:

  • 2 input nodes (สำหรับสองหมายเลขที่จะบวก)
  • 3 hidden nodes (ทำงานหนักด้วยเวทมนตร์ sigmoid)
  • 1 output node (ให้ผลรวมเรา)

นี่คือแผนภาพที่แสดงโครงสร้างของ neural network ที่เรากำลังใช้เพื่อบวกเลขสองจำนวน คุณสามารถเห็นได้ว่าแต่ละ input node เชื่อมต่อกับ hidden layer nodes ทั้งหมด น้ำหนักและอคติถูกนำไปใช้ และทุกอย่างไหลไปยังผลลัพธ์สุดท้าย:

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

นี่คือโค้ดที่เราจะเขียนเพื่อบวกเลขสองจำนวนโดยใช้ neural network:

#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() {
    float weight1_node1 = 0.5f, weight2_node1 = 0.5f, bias_node1 = 0.0f;
    float weight1_node2 = -0.5f, weight2_node2 = -0.5f, bias_node2 = 0.0f;
    float weight1_node3 = 0.25f, weight2_node3 = -0.25f, bias_node3 = 0.0f;

    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; // Normalize target to 0-1 range

                // Forward pass for hidden layer
                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);

                // Output layer with linear activation
                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;

                // Backward pass (output layer - linear)
                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;

                // Backward pass (hidden layer)
                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;

                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;

                // Update weights
                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;
            }
        }

        if ((epoch + 1) % 100 == 0 || epoch == 0)
            cout << "[Summary] 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 << "w1_node1: " << weight1_node1 << ", w2_node1: " << weight2_node1
                             << ", w1_node2: " << weight1_node2 << ", w2_node2: " << weight2_node2
                             << ", w1_node3: " << weight1_node3 << ", w2_node3: " << weight2_node3
                             << ", out_w1: " << weight_out_node1 << ", out_w2: " << weight_out_node2
                             << ", out_w3: " << weight_out_node3 << ", bias_out: " << bias_out
                             << ", bias_node1: " << bias_node1 << ", bias_node2: " << bias_node2 << ", bias_node3: " << bias_node3 << endl;

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

    return 0;
}

แล้ว... มันเรียนรู้ได้อย่างไร?

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

มันก็เหมือนการลองผิดลองถูก แต่ฉลาดกว่า—พร้อมกับการคำนวณเล็กน้อย

มันเรียนรู้การบวกได้จริงหรือ?

เอ่อ ไม่เหมือนที่เราทำ มันไม่เข้าใจว่าคำว่า “บวก” หมายถึงอะไร แต่ มัน เรียนรู้การประมาณที่ดีมาก มันเห็นลวดลายในตัวเลขและคาดการณ์ผลรวมที่มักจะถูกต้อง

มันเหมือนกับการเรียนรู้จากการรู้สึกถึงลวดลายมากกว่าการรู้กฎ หากคุณสนใจ คุณสามารถเปลี่ยนหมายเลขนำเข้าและทดสอบมันได้ ยิ่งคุณเล่นมากเท่าไหร่ คุณก็จะเข้าใจว่าเครือข่ายเล็กๆ เหล่านี้ทำงานอย่างไร

สรุป

การสอน neural network ให้บวกเลขสองจำนวนอาจดูเหมือนเรื่องตลกในตอนแรก แต่เป็นวิธีที่ดีในการมองเข้าไปในจิตใจของ AI เบื้องหลังความฮือฮาทั้งหมด มันก็แค่มีน้ำหนัก อคติ และคณิตศาสตร์อันชาญฉลาด

ในส่วนถัดไป เราจะเจาะลึกลงไปในวิธีการทำงานของ neural networks: เราจะสำรวจเลเยอร์ ฟังก์ชันการกระตุ้น อนุพันธ์ และสิ่งที่เกิดขึ้นจริงในระหว่างการฝึก