PyTorch Basics

Key Concept: นักแคลคูลัสส่วนตัวของคุณ

การสอน AI ให้ฉลาดขึ้น อาศัยกระบวนการที่เรียกว่า Backpropagation ซึ่งเราต้องหา "ค่าความชัน (Gradients)" เพื่อบอกว่าโมเดลควรปรับน้ำหนัก (Weights) ไปทางไหน โชคดีที่เราไม่ต้องนั่งหาอนุพันธ์ (Derivatives) ด้วยคณิตศาสตร์อันซับซ้อนเอง เพราะ PyTorch มีเอนจินที่ชื่อว่า torch.autograd คอยจดจำทุกการคำนวณและหา Gradients ให้เรา "แบบอัตโนมัติ" ทันทีที่เราสั่ง .backward()

1. Forward Pass (เดินหน้า)

รับข้อมูลเข้าโมเดล -> คำนวณผลลัพธ์ -> หาค่าความผิดพลาด (Loss) (Autograd จะแอบจดจำเส้นทางการคำนวณไว้ในพื้นหลัง)

2. Backward Pass (ถอยหลัง)

สั่ง loss.backward() -> Autograd จะวิ่งย้อนกลับตามเส้นทางที่จดไว้เพื่อคำนวณ Gradients ให้กับตัวแปรที่ต้องการปรับปรุง

Tensors, Functions และ Computational Graph

มาดูตัวอย่างการสร้างโมเดล Neural Network แบบง่ายที่สุดที่มีเพียง 1 Layer (1 เลเยอร์) กันครับ ข้อมูลขาเข้า x, ค่าคาดหวัง y, และพารามิเตอร์ w (weights), b (bias)

import torch

x = torch.ones(5)  # ข้อมูล input
y = torch.zeros(3)  # ผลลัพธ์ที่คาดหวัง

# พารามิเตอร์ที่เราต้องการให้โมเดลอัปเดต ต้องตั้งค่า requires_grad=True
w = torch.randn(5, 3, requires_grad=True)
b = torch.randn(3, requires_grad=True)

z = torch.matmul(x, w) + b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)

ในโครงข่ายนี้ w และ b คือ พารามิเตอร์ (Parameters) ที่เราต้องการปรับปรุงค่า (Optimize) ดังนั้นเราจึงต้องสั่งให้ PyTorch ติดตามการเปลี่ยนแปลงของมันโดยระบุ requires_grad=True

ฟังก์ชันที่ใช้บันทึกประวัติ (grad_fn)

เมื่อเราทำการคำนวณ Tensor ที่มี requires_grad=True สิ่งที่ได้กลับมาจะไม่ใช่แค่ตัวเลข แต่เป็น Object ของคลาส Function ซึ่งมันจะเก็บวิธีการคำนวณแบบย้อนกลับไว้ในคุณสมบัติที่ชื่อว่า grad_fn

print(f"Gradient function for z = {z.grad_fn}")
print(f"Gradient function for loss = {loss.grad_fn}")
Output:
Gradient function for z = <AddBackward0 object at 0x7f...>
Gradient function for loss = <BinaryCrossEntropyWithLogitsBackward0 object at 0x7f...>

การคำนวณ Gradients

เพื่อที่จะปรับน้ำหนักของโมเดล เราต้องหาอนุพันธ์ย่อย (Derivatives) ของ Loss เทียบกับพารามิเตอร์แต่ละตัว (คือ $\frac{\partial loss}{\partial w}$ และ $\frac{\partial loss}{\partial b}$) การคำนวณทั้งหมดนี้เกิดขึ้นง่ายๆ เพียงแค่เรียกฟังก์ชัน loss.backward() จากนั้นเราสามารถเข้าไปดูค่าได้ที่ w.grad และ b.grad

loss.backward()

print(f"w.grad (Gradient ของ w) :\n{w.grad}\n")
print(f"b.grad (Gradient ของ b) :\n{b.grad}")
Output:
w.grad (Gradient ของ w) :
tensor([[0.3313, 0.0626, 0.2530],
        [0.3313, 0.0626, 0.2530],
        [0.3313, 0.0626, 0.2530],
        [0.3313, 0.0626, 0.2530],
        [0.3313, 0.0626, 0.2530]])

b.grad (Gradient ของ b) :
tensor([0.3313, 0.0626, 0.2530])

* หมายเหตุ: เราสามารถหาค่า grad ได้เฉพาะ Tensor ปลายทาง (Leaf nodes) ที่เราตั้งค่า requires_grad=True ไว้เท่านั้น และเราเรียก backward() ได้เพียงครั้งเดียวต่อ 1 กราฟ หากต้องการเรียกซ้ำต้องใส่ retain_graph=True

การปิดโหมดติดตาม Gradient (Disabling Gradient Tracking)

โดยค่าเริ่มต้น Tensor ที่ requires_grad=True จะถูกติดตามประวัติการคำนวณเสมอ แต่ในบางกรณี เช่น เมื่อเราฝึกสอนโมเดลเสร็จแล้ว และแค่นำไปใช้งานเพื่อทำนายผล (Inference/Testing) เราไม่ได้ต้องการปรับค่าน้ำหนักอีกต่อไป เราสามารถปิดโหมดนี้ได้เพื่อประหยัดหน่วยความจำและความเร็ว

เราสามารถครอบโค้ดด้วย torch.no_grad() หรือใช้ detach()

# แบบที่ 1: ใช้ torch.no_grad()
z = torch.matmul(x, w) + b
print(f"Requires grad? : {z.requires_grad}")

with torch.no_grad():
    z = torch.matmul(x, w) + b
print(f"Inside no_grad, requires grad? : {z.requires_grad}")

# แบบที่ 2: ใช้ detach()
z_det = z.detach()
print(f"After detach, requires grad? : {z_det.requires_grad}")
Output:
Requires grad? : True
Inside no_grad, requires grad? : False
After detach, requires grad? : False

หลักการทำงานของ Computational Graph (เชิงลึก)

ในทางเทคนิค Autograd เก็บข้อมูลและทุกๆ โอเปอเรชัน (Operation) ไว้ใน Directed Acyclic Graph (DAG)

  • ใบของกราฟ (Leaves) คือ Input Tensors
  • รากของกราฟ (Roots) คือ Output Tensors (ในที่นี้คือ Loss)

เมื่อเราสั่ง .backward() บนรากของกราฟ Autograd จะทำสิ่งเหล่านี้:

  1. คำนวณหา Gradient จากฟังก์ชัน .grad_fn แต่ละตัว
  2. คูณสะสม (Accumulate) Gradient ลงในแอตทริบิวต์ .grad ของแต่ละ Tensor (ตามกฎลูกโซ่ Chain Rule)
  3. ทำงานย้อนกลับไปจนถึงใบของกราฟทั้งหมด