แผนการเรียนรู้
/

PyTorch Neural Network
Multi-Layer Perceptron (MLP)

nn.Module - layers - forward pass - training loop

ผลลัพธ์การเรียนรู้

1. เข้าใจว่า nn.Module คืออะไรและใช้งานอย่างไร
2. สร้าง Multi-Layer Perceptron ด้วย nn.Module
3. แก้ปัญหา XOR ที่ perceptron ทำไม่ได้
4. เขียน training loop ที่สมบูรณ์ด้วย optimizers
สัปดาห์ที่ 10 - 1145208 การเรียนรู้เชิงลึก

เส้นทางของวันนี้: จาก perceptron สู่ MLP

Part 1 - ปัญหาของ Perceptron
ทำไม perceptron ไม่พอ
Perceptron แก้ XOR ไม่ได้, ต้องมี hidden layer
Part 2 - nn.Module
สร้าง MLP ด้วย PyTorch
nn.Module, nn.Linear, nn.ReLU, forward pass
Part 3 - Training Loop
ฝึกสอน MLP
Part 4 - สรุป
สรุป + การบ้าน
เปรียบเทียบ perceptron vs MLP, สรุป pattern, แบบฝึกหัด

Part 1: ทำไม Perceptron ไม่พอ

Perceptron ทำได้

ปัญหาที่แยกได้ด้วยเส้นตรง (linearly separable)

เช่น AND, OR gates

Perceptron ทำไม่ได้

ปัญหาที่แยกไม่ได้ด้วยเส้นตรง

เช่น XOR gate

- XOR: (0,0)->0, (0,1)->1, (1,0)->1, (1,1)->0

- ไม่มีเส้นตรงเดียวที่แยก class 0 กับ class 1 ได้

- Perceptron เรียนรู้ไม่ได้ ไม่ว่าจะฝึกกี่ครั้ง

[ความจริง] Minsky & Papert พิสูจน์ในปี 1969 ว่า perceptron มีข้อจำกัดนี้ ทำให้ AI winter นานหลายปี

XOR Gate - ปัญหาที่ Perceptron ทำไม่ได้

ตารางความจริง

x1  x2  |  y
0   0   |  0
0   1   |  1
1   0   |  1
1   1   |  0

พล็อต

x2
1 |  .  O
  |      
0 |  O  .
   --------
    0     1  x1

O = class 0, . = class 1

- ไม่มีเส้นตรงเดียวที่แยก O กับ . ได้

- ต้องใช้เส้นโค้ง หรือหลายเส้น

- Perceptron มีแค่ 1 layer = แยกไม่ได้

- วิธีแก้: เพิ่ม hidden layer = Multi-Layer Perceptron (MLP)

วิธีแก้: เพิ่ม Hidden Layer

Perceptron (ทำไม่ได้)

Input(2) -> Linear(2,1) -> Output(1)
w1*x1 + w2*x2 + b = y_hat

แยก XOR ไม่ได้

MLP (ทำได้!)

Input(2) -> Linear(2,4) -> ReLU
        -> Linear(4,1) -> Output(1)

ซ่อน layer ระหว่าง input กับ output

- Hidden layer ทำหน้าที่ แปลงข้อมูล ให้แยกได้ด้วยเส้นตรง

- ReLU ทำให้ network ไม่ใช่แค่การแปลงเชิงเส้น

- ยิ่งมี hidden layers มาก = ยิ่งเรียนรู้รูปแบบซับซ้อนได้

- นี่คือจุดเริ่มต้นของ "deep" learning

Part 2: nn.Module - สร้าง model ด้วย class

import torch
import torch.nn as nn

class MyMLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer1 = nn.Linear(2, 4)
        self.layer2 = nn.Linear(4, 1)
        self.relu   = nn.ReLU()
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        x = self.relu(self.layer1(x))
        x = self.sigmoid(self.layer2(x))
        return x

model = MyMLP()
print(model)
print(sum(p.numel() for p in model.parameters()))

- สืบทอดจาก nn.Module

- __init__: กำหนด layers ทั้งหมด

- forward(x): กำหนดว่าข้อมูลไหลอย่างไร

- ห้ามเรียก model.forward(x) โดยตรง! ใช้ model(x)

[เคล็ดลับ] nn.Module จัดการ autograd, device, state_dict ให้อัตโนมัติ

nn.Sequential - วิธีสั้น

# วิธีสั้น: nn.Sequential
model = nn.Sequential(
    nn.Linear(2, 4),
    nn.ReLU(),
    nn.Linear(4, 1),
    nn.Sigmoid()
)

print(model)
# Sequential(
#   (0): Linear(2, 4)
#   (1): ReLU()
#   (2): Linear(4, 1)
#   (3): Sigmoid()
# )

# ใช้เหมือนกัน
output = model(x)

- nn.Sequential จัดเรียง layers ให้อัตโนมัติ

- ข้อมูลไหลตามลำดับ: layer 0 -> 1 -> 2 -> ...

- เหมาะกับ model ที่ไม่ซับซ้อน

- ถ้าต้องการ skip connections หรือ branching ใช้ nn.Module แทน

[เคล็ดลับ] เริ่มจาก Sequential แล้วย้ายไป nn.Module เมื่อ model ซับซ้อนขึ้น

ตัวอย่าง: XOR ด้วย MLP

import torch
import torch.nn as nn

# Data
X = torch.tensor([[0,0],[0,1],[1,0],[1,1]],
                 dtype=torch.float32)
y = torch.tensor([[0],[1],[1],[0]],
                 dtype=torch.float32)

# Model
model = nn.Sequential(
    nn.Linear(2, 8),   # hidden layer ขนาด 8
    nn.ReLU(),
    nn.Linear(8, 1),
    nn.Sigmoid()
)

# Loss + Optimizer
criterion = nn.BCELoss()
optimizer = torch.optim.Adam(
    model.parameters(), lr=0.01)

# Training loop
for epoch in range(1000):
    y_hat = model(X)
    loss = criterion(y_hat, y)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if (epoch+1) % 200 == 0:
        print(f"Epoch {epoch+1}: loss={loss.item():.4f}")

# Test
with torch.no_grad():
    pred = model(X)
    print((pred > 0.5).float())

- ซ่อน layer ขนาด 8 neurons แก้ XOR ได้!

- BCELoss: binary cross-entropy สำหรับ binary output

- optim.Adam: optimizer ที่ปรับ learning rate อัตโนมัติ

- Output: [[0],[1],[1],[0]] = XOR ถูกต้อง!

Layers ที่ใช้บ่อย

nn.Linear

nn.Linear(in, out)

Fully connected layer

y = xW^T + b

nn.ReLU

nn.ReLU()

f(x) = max(0, x)

.activation ที่นิยมที่สุด

nn.Sigmoid

nn.Sigmoid()

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

output เป็น probability

Hidden layer: ใช้ ReLU / Output: ขึ้นกับปัญหา

Part 3: Training Loop ทีละขั้น

# 1. Forward pass
y_hat = model(X)

# 2. Compute loss
loss = criterion(y_hat, y)

# 3. Zero gradients
optimizer.zero_grad()

# 4. Backward pass
loss.backward()

# 5. Update weights
optimizer.step()

Step 1 - Forward: ใส่ข้อมูลเข้า model ได้ output

Step 2 - Loss: คำนวณว่าผิดมากแค่ไหน

Step 3 - Zero grad: ล้าง gradient เก่า (ไม่ล้างจะสะสม!)

Step 4 - Backward: autograd คำนวณ gradient อัตโนมัติ

Step 5 - Update: ปรับ weights ด้วย gradient

[สำคัญ] ลำดับนี้ต้องถูกต้องเสมอ: forward -> loss -> zero_grad -> backward -> step

Optimizers ใน PyTorch

# SGD: พื้นฐาน, ช้าแต่เสถียร
optimizer = torch.optim.SGD(
    model.parameters(), lr=0.01)

# Adam: ปรับ lr อัตโนมัติ, นิยมใช้
optimizer = torch.optim.Adam(
    model.parameters(), lr=0.001)

# AdamW: Adam + weight decay
optimizer = torch.optim.AdamW(
    model.parameters(), lr=0.001,
    weight_decay=0.01)

# ดู learning rate ปัจจุบัน
for param_group in optimizer.param_groups:
    print(param_group['lr'])

- SGD: เรียบง่าย, lr คงที่

- Adam: ปรับ lr ให้แต่ละ parameter, เร็วกว่า, แนะนำเป็นค่าเริ่มต้น

- AdamW: Adam + regularization

- lr 0.001 เป็นค่าเริ่มต้นที่ดีสำหรับ Adam

[เคล็ดลับ] เริ่มจาก Adam(lr=0.001) แล้วค่อย tune ถ้าผลไม่ดี

Loss Functions ที่ใช้บ่อย

Classification

BCELoss() -- binary, output sigmoid
BCEWithLogitsLoss() -- binary, output ไม่ผ่าน sigmoid
CrossEntropyLoss() -- multi-class, output raw logits

Regression

MSELoss() -- Mean Squared Error
L1Loss() -- Mean Absolute Error
HuberLoss() -- รวม MSE + MAE

CrossEntropyLoss = LogSoftmax + NLLLoss (ใส่ raw logits ไม่ต้อง softmax ก่อน!)

ตัวอย่างสมบูรณ์: XOR MLP

import torch
import torch.nn as nn

# Data
X = torch.tensor([[0,0],[0,1],[1,0],[1,1]], dtype=torch.float32)
y = torch.tensor([[0],[1],[1],[0]], dtype=torch.float32)

# Model
model = nn.Sequential(
    nn.Linear(2, 8),
    nn.ReLU(),
    nn.Linear(8, 1),
    nn.Sigmoid()
)

# Loss + Optimizer
criterion = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

# Training
for epoch in range(1001):
    y_hat = model(X)
    loss = criterion(y_hat, y)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if epoch % 200 == 0:
        print(f"Epoch {epoch:4d} | loss: {loss.item():.4f}")

# Evaluate
with torch.no_grad():
    pred = model(X)
    print("\nPredictions:")
    for i in range(4):
        x1, x2 = X[i].tolist()
        p = pred[i].item()
        print(f"  ({int(x1)},{int(x2)}) -> {p:.4f} -> {int(p > 0.5)}")
#   (0,0) -> 0.0012 -> 0
#   (0,1) -> 0.9987 -> 1
#   (1,0) -> 0.9989 -> 1
#   (1,1) -> 0.0014 -> 0

Django Dashboard + SSE

uv add django

uv run django-admin startproject wk10 .
uv run manage.py startapp dashboard

# wk10/urls.py
from django.urls import path, include
urlpatterns = [
    path('', include('dashboard.urls')),
]

# dashboard/views.py
import json, torch, torch.nn as nn
from django.http import StreamingHttpResponse

def train(request):
    def event_stream():
        X = torch.tensor([[0,0],[0,1],
                          [1,0],[1,1]], dtype=torch.float32)
        y = torch.tensor([[0],[1],[1],[0]],
                         dtype=torch.float32)
        model = nn.Sequential(
            nn.Linear(2, 8), nn.ReLU(),
            nn.Linear(8, 1), nn.Sigmoid())
        criterion = nn.BCELoss()
        optimizer = torch.optim.Adam(model.parameters(), lr=0.05)
        for epoch in range(201):
            loss = criterion(model(X), y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            with torch.no_grad():
                pred = model(X).squeeze().tolist()
            yield f"data: {json.dumps({'epoch': epoch, 'loss': loss.item(), 'pred': pred})}\n\n"
    return StreamingHttpResponse(event_stream(), content_type='text/event-stream')

- Django: web framework สำหรับ backend

- SSE (Server-Sent Events): server ส่งข้อมูล real-time ให้ client ผ่าน EventSource

- StreamingHttpResponse: ส่ง training update ทีละ epoch ผ่าน SSE

- Frontend ใช้ Canvas แสดง XOR scatter plot

[โครงสร้าง] manage.py -> settings.py -> urls.py -> views.py -> templates/

torch.no_grad() - ประเมิน model

# ฝึกสอน: ต้องการ gradient
for epoch in range(1000):
    y_hat = model(X)           # forward
    loss = criterion(y_hat, y)
    optimizer.zero_grad()
    loss.backward()            # คำนวณ gradient
    optimizer.step()           # อัปเดต

# ประเมิน: ไม่ต้องการ gradient
with torch.no_grad():         # ปิด gradient
    y_hat = model(X)
    accuracy = (y_hat > 0.5).float().eq(y).mean()
    print(f"Accuracy: {accuracy:.4f}")

# model.eval() / model.train()
model.eval()                  # ประเมิน mode
y_hat = model(X)

model.train()                 # ฝึก mode
y_hat = model(X)

- torch.no_grad(): ปิดการคำนวณ gradient = เร็วขึ้น, ประหยัด memory

- ใช้ตอน evaluate, predict, ไม่ใช่ตอน train

- model.eval(): เปลี่ยนเป็น evaluation mode

- model.train(): เปลี่ยนกลับเป็น training mode

[สำคัญ] eval() สำคัญสำหรับ Dropout/BatchNorm ที่ทำงานต่างกันระหว่าง train/eval

บันทึกและโหลด Model

# บันทึก weights (แนะนำ)
torch.save(model.state_dict(),
           'model_weights.pth')

# โหลด weights
model = MyMLP()               # สร้าง model ก่อน
model.load_state_dict(
    torch.load('model_weights.pth'))
model.eval()

# บันทึก model ทั้งหมด
torch.save(model, 'model_full.pth')

# โหลด model ทั้งหมด
model = torch.load('model_full.pth')

- state_dict(): dictionary ของ weights ทั้งหมด

- แนะนำบันทึกแบบ state_dict (ยืดหยุ่นกว่า)

- บันทึก: .pth หรือ .pt

- โหลด: ต้องสร้าง model ก่อน แล้ว load_state_dict()

[สำคัญ] ต้องสร้าง model architecture ก่อนโหลด weights!

Part 4: Perceptron vs MLP

Perceptron

  • - 1 layer: input -> output
  • - แยกได้เฉพาะ linearly separable
  • - AND, OR ได้ / XOR ไม่ได้
  • - ไม่มี activation function

MLP (Multi-Layer Perceptron)

  • - หลาย layers: input -> hidden -> output
  • - แยกได้ทุกปัญหา (universal approximator)
  • - XOR, รูปภาพ, ข้อความ ได้หมด
  • - มี ReLU, Sigmoid, etc.

Hidden layers คือสิ่งที่ทำให้ "deep" learning เป็น "deep"

สรุป + การบ้าน

สิ่งที่เรียนวันนี้

  • - Perceptron แก้ XOR ไม่ได้ = ต้องมี hidden layer
  • - nn.Module = class สำหรับสร้าง model
  • - nn.Sequential = วิธีสั้น
  • - Training loop: forward -> loss -> zero_grad -> backward -> step
  • - optim.Adam = optimizer แนะนำ
  • - Loss functions: BCELoss, CrossEntropyLoss

การบ้าน (ส่งสัปดาห์หน้า)

  • - สร้าง MLP สำหรับ Breast Cancer dataset
  • - Input 30 features -> Hidden(16) -> ReLU -> Hidden(8) -> ReLU -> Output(1) -> Sigmoid
  • - ใช้ Adam optimizer, BCELoss
  • - วาด loss curve
  • - บันทึก weights ด้วย state_dict()

สัปดาห์หน้า: Dataset/DataLoader และ classification จริงด้วย PyTorch