ยินดีต้อนรับกลับ! ในตอนที่ 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 ซึ่งออกแบบมาเพื่อรักษาสัญญาณที่เสถียรขณะที่มันไหลผ่านเครือข่าย สำหรับการทดลองง่ายๆ เช่นของเรา ค่าที่ตั้งค่าแบบแมนนวลหรือค่าขนาดเล็กแบบสุ่มก็เพียงพอแล้ว.
เราปรับน้ำหนักอย่างไร?
ระหว่างการฝึก นี่คือวิธีการปรับ:
- เครือข่ายทำการคาดการณ์โดยใช้น้ำหนักปัจจุบัน.
- มันเปรียบเทียบการคาดการณ์กับคำตอบที่ถูกต้อง (เป้าหมาย).
- มันคำนวณว่ามันผิดพลาดแค่ไหน — นี่คือความผิดพลาดหรือ "ความสูญเสีย".
- มันใช้ backpropagation เพื่อคำนวณว่าน้ำหนักแต่ละตัวมีผลต่อความผิดพลาดอย่างไร.
- น้ำหนักแต่ละตัวจะถูกปรับเล็กน้อยเพื่อลดความผิดพลาดในครั้งถัดไป — ขั้นตอนนี้ทำโดยใช้เกรด (อนุพันธ์) และ 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").

เมื่อไหร่ควรใช้ฟังก์ชันการเปิดใช้งานใด?
ฟังก์ชันการเปิดใช้งาน | ช่วงผลลัพธ์ | เหมาะสำหรับ | ประเภทเลเยอร์ทั่วไป | หมายเหตุ |
---|---|---|---|---|
Sigmoid | 0 ถึง 1 | ผลลัพธ์แบบไบนารี | เลเยอร์เอาต์พุต | อาจทำให้เกิดความชันที่หายไป |
Tanh | -1 ถึง 1 | เลเยอร์ที่ซ่อนที่มีศูนย์กลางที่ 0 | เลเยอร์ที่ซ่อน | ความชันที่แข็งแกร่งกว่าซิกมอยด์ |
ReLU | 0 ถึง ∞ | เครือข่ายลึกและ CNN ส่วนใหญ่ | เลเยอร์ที่ซ่อน | รวดเร็วและมีประสิทธิภาพ แต่สามารถ "ตาย" ได้เมื่อเป็นค่าลบ |
ในโค้ดตัวอย่างของเรา เราใช้ sigmoid สำหรับเลเยอร์ที่ซ่อน มันทำงานได้ดีสำหรับการสาธิตขนาดเล็ก แต่ในเครือข่ายที่ใหญ่ขึ้น ReLU มักจะถูกเลือกเพื่อประสิทธิภาพที่ดีกว่าและการฝึกที่เร็วขึ้น.
อนุพันธ์และ Backpropagation: วิธีที่เครือข่ายเรียนรู้จากความผิดพลาด
ลองนึกภาพว่าคุณถูกปิดตา ยืนอยู่บนเขา และต้องหาทางลงเขา คุณจะรู้สึกไปรอบๆ ด้วยเท้าเพื่อสัมผัสว่าทิศทางไหนลาดลง จากนั้นจึงเคลื่อนไปในทิศทางนั้น.
นั่นคือสิ่งที่ neural networks ทำโดยใช้อนุพันธ์—พวกเขาตรวจสอบว่าการปรับเล็กน้อยใดทำให้การคาดการณ์แม่นยำขึ้น.
หลังจากทำการคาดการณ์ เครือข่ายจะคำนวณว่ามันผิดพลาดแค่ไหน (เราจะเรียกสิ่งนี้ว่า "ความสูญเสีย") จากนั้นมันจะใช้สิ่งที่เรียกว่า backpropagation เพื่อติดตามความผิดพลาดนั้นย้อนกลับผ่านเครือข่าย โดยระบุว่าแต่ละน้ำหนักมีส่วนช่วยต่อความผิดพลาดอย่างไร.
นี่คือลูป backpropagation ที่เรียบง่าย:
- การส่งข้อมูลไปข้างหน้า: เดาคำตอบ.
- คำนวณความสูญเสีย: การเดานั้นผิดพลาดแค่ไหน?
- การส่งข้อมูลย้อนกลับ: ค้นหาว่าน้ำหนักแต่ละตัวมีผลต่อความผิดพลาดอย่างไร.
- อัปเดตน้ำหนัก: ปรับน้ำหนักเล็กน้อยเพื่อลดความผิดพลาดในอนาคต.
กระบวนการนี้จะทำซ้ำหลายพันครั้ง ทำให้เครือข่ายฉลาดขึ้นเรื่อยๆ.
มาดูการนำไปใช้ 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 ถึงเติบโตอย่างรวดเร็วในช่วงไม่กี่ปีที่ผ่านมา? อะไรเปลี่ยนไป? เราจะสำรวจว่าทุกอย่างเริ่มต้นอย่างไร—และทำไมมันถึงเพิ่งเริ่มต้น.