torch.nn ได้uv init -> uv add torch -> รันได้บนทุกเครื่องเหมือนกันnn.Linear + nn.Sigmoiduv คือตัวจัดการโปรเจกต์ Python แบบ all-in-one (แทน pip + venv + requirements.txt)
$ /bin/bash -c "$(curl -fsSL \
https://raw.githubusercontent.com/\
Homebrew/install/HEAD/install.sh)"
$ brew install uv
$ uv --version
uv 0.9.x
$ curl -LsSf \
https://astral.sh/uv/install.sh | sh
$ uv --version
uv 0.9.x
PS> Set-ExecutionPolicy RemoteSigned `
-Scope CurrentUser
PS> Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
PS> scoop install git # git ก่อน
PS> scoop install uv
PS> uv --version
[OK] เมื่อ uv --version ทำงานได้ = พร้อมไปต่อ
uv init$ uv init --no-package perceptron $ cd perceptron perceptron/ ├── .python-version # lock เวอร์ชัน Python ├── main.py # ไฟล์เริ่มต้น └── pyproject.toml # ศูนย์กลาง config
- pyproject.toml คือบัตรประชาชนของโปรเจกต์ -- บอกชื่อ, เวอร์ชัน, dependencies
- .python-version ทำให้ทุกเครื่องใช้ Python เวอร์ชันเดียวกัน
- ไม่ต้องสร้าง venv เอง -- uv จัดการให้อัตโนมัติ
uv add torch$ uv add torch
Resolved 12 packages
+ numpy==2.x.x
+ sympy==1.14.0
+ torch==2.x.x
$ uv run main.py # รันใน venv ของโปรเจกต์
Hello from perceptron-lab!
- uv สร้าง virtualenv + ติดตั้ง + บันทึกเวอร์ชันลง uv.lock ให้เอง
- uv run ทุกครั้ง = ไม่มีปัญหา "รันใน env ผิด"
[Tip] เครื่องที่ clone ไปแค่ uv sync ก็ได้ environment เหมือนกันเป๊ะ
# check.py
import torch
print("torch:", torch.__version__)
print("device:",
"cuda" if torch.cuda.is_available()
else "mps" if torch.backends.mps.is_available()
else "cpu")
t = torch.tensor([[1.0, 2.0],
[3.0, 4.0]])
print(t @ t.T) # matrix multiply
- Tensor คือ n-dimensional array ของ PyTorch (คล้าย ndarray ของ NumPy)
- tensor เก็บ computation graph ไว้ให้ด้วย -> คำนวณ derivative อัตโนมัติ (autograd)
$ uv run check.py
torch: 2.x.x
device: cpu
tensor([[ 5., 11.],
[11., 25.]])
เพอร์เซปตรอน = ฟังก์ชันทางคณิตศาสตร์ชุดเดียว
\(z = w \cdot x + b\)
weighted sum ของ input ทุกตัว บวก bias
features ของข้อมูล เช่น (รัศมีเนื้องอก, ขนาด) = [1.2, 0.8]
ตัวเลขที่โมเดลจะเรียนรู้ -- ยิ่ง w มาก feature นั้นยิ่งมีน้ำหนัก
z เป็นตัวเลขเท่าไรก็ได้ -> ต้องแปลงเป็น "ความมั่นใจ" ก่อน
\(\hat{y} = 1 \text{ if } z \geq 0, \text{ else } 0\)
ตัดสินใจเด็ดขาด แต่... ไม่มีอนุพันธ์ -> ฝึกด้วย gradient descent ไม่ได้
\(\sigma(z) = \dfrac{1}{1 + e^{-z}}\)
บีบค่าเข้าช่วง (0, 1) = probability [OK] smooth [OK] มีอนุพันธ์ทุกจุด [OK]
ความนุ่มของ sigmoid คือกุญแจที่ทำให้ "การเรียนรู้" เป็นไปได้
ยิ่ง loss น้อย ยิ่ง prediction ตรงกับคำตอบจริง
\(L = \dfrac{1}{N} \sum (\hat{y}_i - y_i)^2\)
ระยะห่างกำลังสองระหว่างทำนาย vs จริง
\(L = -\big[y \log \hat{y} + (1-y) \log(1-\hat{y})\big]\)
ทำนายผิดฝั่ง = ลงโทษหนักมาก (log พุ่งไป -inf)
[Goal] เป้าหมายของ training: ปรับ w, b ให้ L ต่ำที่สุด
\(w \leftarrow w - \eta \cdot \dfrac{\partial L}{\partial w}\)
ขยับ weight ทีละน้อย ตามทิศทางที่ loss ลดลง
อนุพันธ์ = ทิศทาง + ความชัน ที่บอกว่า loss จะแยก/ดีขึ้นถ้าขยับ w
ก้าวใหญ่ไป = กระโดดข้ามหลุม, ก้าวเล็กไป = ช้ามาก (เราใช้ 0.1)
loss.backward() คำนวณอนุพันธ์ทุกตัวแทนเรา -- ไม่ต้องไล่สมการเอง
[Loop] repeat: forward -> backward -> update จน loss ลดพอใจ
สมการ \(\hat{y} = \sigma(w \cdot x + b)\)
-> nn.Linear(2, 1) = \(w \cdot x + b\)
(เก็บ w, b ไว้ให้และเรียนรู้เอง)
-> nn.Sigmoid() = \(\sigma(z)\)
import torch
model = torch.nn.Sequential(
torch.nn.Linear(2, 1), # z = w·x + b
torch.nn.Sigmoid(), # ŷ = σ(z)
)
# ลอง peek ค่าเริ่มต้น:
print(model[0].weight) # tensor([[...]])
print(model[0].bias) # tensor([...])
y_hat = model(torch.randn(5, 2)) # ทาย 5 จุด
model เริ่มจาก w, b สุ่ม ๆ -- ยัง "โง่" อยู่ ต่อไปคือทำให้ฉลาด
import torch
torch.manual_seed(42)
# ข้อมูลจำลอง: 200 จุดใน 2 มิติ แบ่งด้วยเส้นตรง 1.5x + y > 0.5
N = 200
X = torch.randn(N, 2)
y = ((X[:, 0] * 1.5 + X[:, 1] - 0.5)
> 0).float().unsqueeze(1)
model = torch.nn.Sequential(
torch.nn.Linear(2, 1),
torch.nn.Sigmoid(),
)
loss_fn = torch.nn.BCELoss()
opt = torch.optim.SGD(model.parameters(), lr=0.1)
for epoch in range(200):
[!] ลืม zero_grad() = gradient ทับกัน โมเดลพังแบบเข้าใจยาก!
z = w₁·x₁ + w₂·x₂ + b ← linear a = sigmoid(z) ← activation L = BCE(a, y) ← loss
dL/da = ... ← loss → activation da/dz = sigmoid'(z) ← activation → z dz/dw₁ = x₁ ← z → w₁ dL/dw₁ = dL/da · da/dz · dz/dw₁ ← chain rule!
[Tip] Chain rule คูณ derivatives ย้อนกลับทีละชั้น -- เหมือนกระโดด rope ย้อนจาก loss กลับมายัง w ทุกตัว
- Forward pass: ใส่ข้อมูล -> โมเดลคำนวณ -> ได้คำตอบ + loss
- Backward pass: จาก loss คำนวณย้อนกลับว่า "w แต่ละตัวมีส่วนทำให้ loss ผิดไปเท่าไหร่"
- \(\frac{\partial L}{\partial w}\) = ถ้าเพิ่ม w นิดเดียว loss จะเปลี่ยนเท่าไหร่ -> ทิศทางที่ควรปรับ
- โมเดล 1 ชั้น = chain rule 1 ชั้น / โมเดล 100 ชั้น = chain rule 100 ชั้น -> นี่คือ "deep" ใน deep learning
[Tip] ย้อนกลับจาก loss -> output -> hidden -> input เรียกว่า "back" propagation เพราะเริ่มจาก "back" (ท้าย)
import torch # 1) requires_grad=True → ให้ PyTorch ติดตาม w = torch.tensor(1.0, requires_grad=True) # 2) Forward pass — PyTorch สร้าง computational graph x = torch.tensor(2.0) z = w * x # z = 2w loss = (z - 3)**2 # loss = (2w - 3)² # 3) Backward pass — คำนวณ dloss/dw อัตโนมัติ loss.backward() print(w.grad) # tensor(4.) ← dloss/dw = 4(2w-3) # 4) Update — optimizer ใช้ gradient ปรับ w opt = torch.optim.SGD([w], lr=0.1) opt.step() # w = 1.0 - 0.1*4.0 = 0.6
- requires_grad=True = ป้ายบอก PyTorch "เก็บ operation ทุกขั้นตอนไว้"
- ทุกครั้งที่คำนวณ (forward) PyTorch สร้าง computational graph เบื้องหลัง
- loss.backward() = เดินย้อน graph เรียก chain rule อัตโนมัติ -> ผลอยู่ใน w.grad
- optimizer.step() = ใช้ gradient ปรับน้ำหนัก: w = w - lr * grad
- zero_grad() = ล้าง gradient เก่าก่อนรอบใหม่ -- ไม่งั้น gradient บวกทับกัน
[Tip] ใน train.py: 3 บรรทัดนี้ = หัวใจของการเทรน -- loss.backward() + opt.step() + zero_grad()
import torch.nn as nn
model = nn.Sequential(
nn.Linear(2, 1), # ← กำหนด (in, out)
nn.Sigmoid(),
)
print(model)
# Linear(in_features=2,
# out_features=1)
- nn.Linear(2,1) -- บอก input/output ตรง ๆ ไม่ต้องมี input_shape
- ไม่มี summary() สำเร็จรูป ต้อง print(model) เอง
import keras
model = keras.Sequential([
keras.layers.Dense(1,
activation='sigmoid',
input_shape=(2,)), # ← กำหนด input
])
model.summary()
# Layer (type) Output Param
# dense (Dense) (None,1) 3
- input_shape=(2,) -- บอก shape อินพุตครั้งเดียวในชั้นแรก
- model.summary() -- นับพารามิเตอร์ให้ทันที (2*1+1=3)
/Instruction เหมือนกัน: กำหนดชั้น -> ใส่ activation -> เตรียมเทรน ต่างกันแค่ syntax
loss_fn = nn.BCELoss()
opt = torch.optim.SGD(
model.parameters(), lr=0.1)
for epoch in range(200):
y_hat = model(X) # 1. forward
loss = loss_fn(y_hat, y) # 2. คำนวณ loss
opt.zero_grad() # 3. ล้าง gradient
loss.backward() # 4. autograd
opt.step() # 5. update w,b
[OK] เข้าถึงทุกขั้นตอน -- custom loop, multi-task, debug ง่าย
[!] เขียนมากกว่า -- ลืม zero_grad ได้
model.compile(
loss='binary_crossentropy',
optimizer='sgd',
metrics=['accuracy']
)
model.fit(X, y,
epochs=200,
verbose=1)
# loss: 0.22 - acc: 0.99
[OK] 2 บรรทัดจบ -- callbacks, tensorboard สำเร็จรูป
[!] ซ่อนรายละเอียด -- ปรับ custom training ยากกว่า
PyTorch = ขับรถเอง (ควบคุมทุกอย่าง) - Keras = นั่งแท็กซี่ (สะดวก แต่เลือกทางไม่ได้)
ต้องเพิ่มไลบรารีวาดกราฟก่อน: uv add matplotlib
# viz.py — ดูว่าโมเดล "เดา" ได้แย่แค่ไหน
import matplotlib.pyplot as plt
import torch
torch.manual_seed(42)
N = 200
X = torch.randn(N, 2)
y = ((X[:, 0] * 1.5 + X[:, 1] - 0.5)
> 0).float().unsqueeze(1)
model = torch.nn.Sequential(
torch.nn.Linear(2, 1),
torch.nn.Sigmoid(),
) # ยังไม่เทรนเลย!
Xp, yp = X.numpy(), y.numpy().ravel()
plt.scatter(Xp[yp == 1, 0], Xp[yp == 1, 1],
c="tab:green", s=15)
plt.scatter(Xp[yp == 0, 0], Xp[yp == 0, 1],
c="tab:red", s=15)
plt.show()
- เขียว = class 1, แดง = class 0 -- ข้อมูลแบ่งด้วยเส้นตรงได้
- ก่อนเทรน w, b เป็นค่าสุ่ม -> เส้นแบ่งวิ่งพาดผิดที่
[Tip] เป้าหมายของ training ในสายตา: หมุน + เลื่อนเส้นนี้ไปอยู่กึ่งกลางสองฝั่งให้ได้
เส้นแบ่ง = จุดที่โมเดลเดาไม่ออกว่าฝั่งไหน คือ \(\sigma(z) = 0.5 \iff z = 0\)
\(w_1 x_1 + w_2 x_2 + b = 0 \;\Longrightarrow\; x_2 = -\dfrac{w_1 x_1 + b}{w_2}\)
def plot_boundary(model):
w = model[0].weight.data.ravel()
b = model[0].bias.data.item()
xs = torch.linspace(-3, 3, 2)
ys = -(w[0] * xs + b) / w[1]
plt.plot(xs.numpy(), ys.numpy(),
"k-", lw=2)
plot_boundary(model) # ลองก่อนเทรน!
plt.show()
- ดึง weight, bias ออกจาก nn.Linear (ชั้นแรก = model[0])
- แทนจุด x1 สองค่า (-3, 3) -> คำนวณ x2 -> ได้เส้นตรง 2 จุด
- ฟังก์ชันนี้ reuse ได้ทุกตอนที่อยาก "ถ่ายรูป" โมเดล
for epoch in range(200):
y_hat = model(X)
loss = loss_fn(y_hat, y)
opt.zero_grad()
loss.backward()
opt.step()
if epoch % 10 == 0: # ทุก 10 epoch
plt.clf() # เคลียร์กราฟเดิม
plt.scatter(Xp[yp == 1, 0],
Xp[yp == 1, 1], c="tab:green", s=15)
plt.scatter(Xp[yp == 0, 0],
Xp[yp == 0, 1], c="tab:red", s=15)
plot_boundary(model)
plt.title(
f"epoch {epoch} | loss {loss.item():.3f}")
plt.pause(0.001) # ให้หน้าต่าง "เคลื่อน" สด ๆ
[See] สิ่งที่จะเห็น: เส้นสุ่ม ๆ -> หมุน+เลื่อนทีละนิด -> นิ่งในตำแหน่งแบ่งสองคลาสสวยงาม (loss ลดลงพร้อมกัน)
- plt.pause(0.001) = บังคับ render ใหม่ในหน้าต่างเดิม
[Tip] อยากได้ GIF แนบรายงาน? เปลี่ยน pause เป็น plt.savefig(f"e{epoch}.png") แล้วต่อเฟรมด้วย imageio
# views.py
import threading, json, torch
from django.http import JsonResponse
_state = dict(epoch=0, loss=0, acc=0,
w=[0.0, 0.0], b=0.0, running=False)
def _train():
torch.manual_seed(42)
X = torch.randn(200, 2)
y = ((X[:,0]*1.5+X[:,1]-0.5)>0
).float().unsqueeze(1)
model = torch.nn.Sequential(
torch.nn.Linear(2,1), torch.nn.Sigmoid())
loss_fn = torch.nn.BCELoss()
opt = torch.optim.SGD(model.parameters(), lr=0.1)
_state['running'] = True
for ep in range(200):
y_hat = model(X)
loss = loss_fn(y_hat, y)
opt.zero_grad(); loss.backward(); opt.step()
_state.update(
epoch=ep,
loss=round(loss.item(),4),
acc=round(((y_hat>0.5).float()==y
).float().mean().item(),4),
w=[round(x,4) for x in
model[0].weight.data.tolist()[0]],
b=round(model[0].bias.data.item(),4))
_state['running'] = False
def train_start(request):
threading.Thread(target=_train, daemon=True).start()
return JsonResponse({'ok': True})
def train_state(request):
return JsonResponse(_state)
- _state = ตัวแปรกลางเก็บสถานะโมเดลล่าสุด -- JS จะดึงตัวนี้
- threading.Thread = train ในเบื้องหลัง ไม่บล็อก Django server
- /api/train/ = ปุ่มเริ่มเทรน (POST)
- /api/state/ = JS polling ทุก 50ms ดึง loss, acc, w, b
[!] ข้อเสีย: JS ต้องยิง request ซ้ำ -- เปลือง bandwidth, lag ตาม polling interval
<canvas id="cv" width="400" height="400"></canvas>
<div id="info"></div>
<script>
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const CX = 200, S = 60;
const pts = [];
function randn(){let u=0,v=0;while(!u)u=Math.random();
v=Math.random();return Math.sqrt(-2*Math.log(u))*Math.cos(6.283*v)}
for(let i=0;i<200;i++){
const x=randn(),y=randn();
pts.push({x,y,l:(x*1.5+y-0.5)>0?1:0})}
function draw(s){
ctx.fillStyle='#1a1a2e'; ctx.fillRect(0,0,400,400);
for(const p of pts){
ctx.beginPath();
ctx.arc(CX+p.x*S, CX-p.y*S, 3, 0, 6.283);
ctx.fillStyle=p.l?'#4ade80':'#f87171'; ctx.fill();}
if(s.w && s.w[1]!==0){
ctx.beginPath();
ctx.moveTo(0, CX-(-(s.w[0]*(-3)+s.b)/s.w[1])*S);
ctx.lineTo(400,CX-(-(s.w[0]*( 3)+s.b)/s.w[1])*S);
ctx.strokeStyle='#fbbf24'; ctx.lineWidth=2; ctx.stroke();}
document.getElementById('info').textContent=
`epoch ${s.epoch} | loss ${s.loss} | acc ${(s.acc*100).toFixed(0)}%`;}
// polling loop — ยิง request ซ้ำทุก 50ms
async function poll(){
const r = await fetch('/api/state/');
const s = await r.json();
draw(s);
if(s.running) setTimeout(poll, 50);}
fetch('/api/train/').then(()=>poll());
</script>
- Canvas = ผ้าใบวาดด้วย JS -- เร็วกว่า DOM manipulation มาก
- สูตรเส้นแบ่ง: y = -(w1x + b) / w2 แทนค่าจาก /api/state/
- setTimeout(poll, 50) = ดึงทุก 50ms -> กราฟลื่น ~20fps
[!] ทุก 50ms = วินาทีละ 20 request -- ถ้ามี 100 คนดู = 2000 request/วินาที!
# views.py import json, torch from django.http import StreamingHttpResponse def train_stream(request): def generate(): torch.manual_seed(42) X = torch.randn(200, 2) y = ((X[:,0]*1.5+X[:,1]-0.5)>0 ).float().unsqueeze(1) model = torch.nn.Sequential( torch.nn.Linear(2,1), torch.nn.Sigmoid()) loss_fn = torch.nn.BCELoss() opt = torch.optim.SGD(model.parameters(), lr=0.1) for ep in range(200): y_hat = model(X) loss = loss_fn(y_hat, y) opt.zero_grad(); loss.backward(); opt.step() yield "data: " + json.dumps(dict( epoch=ep, loss=round(loss.item(),4), acc=round(((y_hat>0.5).float()==y ).float().mean().item(),4), w=[round(x,4) for x in model[0].weight.data.tolist()[0]], b=round(model[0].bias.data.item(),4) )) + "\n\n" return StreamingHttpResponse( generate(), content_type='text/event-stream')
- StreamingHttpResponse = ส่งข้อมูลทีละชิ้นโดยไม่ปิด connection
- yield = สร้าง generator -- ส่ง JSON ทุก epoch แล้วรอ
- text/event-stream = รูปแบบ SSE (Server-Sent Events) ที่ browser รู้จัก
- ไม่ต้อง threading -- generator ทำงานใน request cycle ของ Django เอง
[Tip] SSE ต่างจาก polling: ไม่ต้องยิง request ซ้ำ -- server ส่งข้อมูลมาเองเมื่อพร้อม
<canvas id="cv" width="400" height="400"></canvas>
<div id="info"></div>
<script>
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const CX = 200, S = 60;
// ข้อมูล (seed เดียวกับ backend)
const pts = [];
function randn(){let u=0,v=0;while(!u)u=Math.random();
v=Math.random();return Math.sqrt(-2*Math.log(u))*Math.cos(6.283*v)}
for(let i=0;i<200;i++){
const x=randn(),y=randn();
pts.push({x,y,l:(x*1.5+y-0.5)>0?1:0})}
function draw(s){
ctx.fillStyle='#1a1a2e'; ctx.fillRect(0,0,400,400);
for(const p of pts){
ctx.beginPath();
ctx.arc(CX+p.x*S, CX-p.y*S, 3, 0, 6.283);
ctx.fillStyle=p.l?'#4ade80':'#f87171'; ctx.fill();}
if(s.w && s.w[1]!==0){
ctx.beginPath();
ctx.moveTo(0, CX-(-(s.w[0]*(-3)+s.b)/s.w[1])*S);
ctx.lineTo(400,CX-(-(s.w[0]*( 3)+s.b)/s.w[1])*S);
ctx.strokeStyle='#fbbf24'; ctx.lineWidth=2; ctx.stroke();}
document.getElementById('info').textContent=
`epoch ${s.epoch} | loss ${s.loss} | acc ${(s.acc*100).toFixed(0)}%`;}
// EventSource = browser API สำหรับรับ SSE stream
const es = new EventSource('/api/train/');
es.onmessage = (e) => draw(JSON.parse(e.data));
es.onerror = () => es.close(); // จบ stream → ปิด
</script>
- EventSource = browser API สำหรับรับ SSE -- ไม่ต้องเขียน loop เอง
- onmessage = callback ที่เรียกทุกครั้งที่ server ส่ง data: ... มา
- สูตรเส้นแบ่ง: y = -(w1x + b) / w2 แทนค่าจาก stream ตรง ๆ
- ไม่ต้อง setTimeout -- browser จัดการ timing ให้เอง
[Tip] ลองเปิด /dashboard/ บน 2 เบราว์เซอร์พร้อมกัน -- เห็น boundary ขยับพร้อมกัน realtime!
uv run train.py$ uv run train.py epoch 40 | loss 0.3940 | acc 94.50% epoch 80 | loss 0.3074 | acc 97.50% epoch 120 | loss 0.2647 | acc 100.00% epoch 160 | loss 0.2379 | acc 99.50% epoch 200 | loss 0.2190 | acc 99.00% w = tensor([[2.1772, 1.4041]]) b = tensor([-0.7237])
[-] loss ลด 0.39 -> 0.22 และ [+] accuracy ขึ้น 94% -> 99% -- โมเดลเรียนรู้จริง
[>] สายตรงจริงคือ (1.5, 1.0), b=-0.5 -- โมเดลเรียนได้ (2.18, 1.40), b=-0.72 ซึ่งเป็นเส้นเดียวกัน (scale ต่างกันเท่านั้น)
[Done] คุณเพิ่งฝึก neural network ตัวแรกด้วย PyTorch สำเร็จ!
[See] รันพร้อมโค้ดจาก Step 1-3 จะเห็นหน้าต่างกราฟวาดเส้นแบ่งใหม่ทุก 10 epoch จนนิ่ง
def make_xor_data(n=200):
"""XOR: ช่อง 1,3 = 1, ช่อง 2,4 = 0
→ เส้นตรงเดียวแบ่งไม่ได้แน่นอน"""
X = torch.rand(n, 2) * 4 - 2 # [-2, 2]
y = (X[:, 0] * X[:, 1] > 0).float()
return X, y.unsqueeze(1)
X_xor, y_xor = make_xor_data()
# scatter plot: สีเขียว=1, แดง=0
# จุดในเขตรวมสีเดียวกัน = XOR = ทแยงกัน
# → เส้นตรงเส้นเดียวไม่มีวันแบ่งถูกทั้งหมด
- สูตร x1 * x2 > 0 หมายถึงจุดอยู่ในช่องที่ 1 หรือ 3 (ค่าเท่ากัน)
- ถ้าวาด scatter จุดสีแดงจะอยู่ช่อง 2,4 และเขียวช่อง 1,3 -> " checkerboard "
- เส้นตรงที่ลากตัด ctxt ได้ดีสุด = 50% แบ่งไม่ได้มากกว่านั้น -- Minsky & Papert 1969 ใช้ XOR เป็นหลักฐานว่าเพอร์เซปตรอนมีขีดจำกัด
[Tip] วิธีพลิก: hidden layer พับพื้นที่ให้ XOR กลายเป็น linearly separable ได้ คือหัวใจของ "deep learning"
สัปดาห์หน้า: Dataset/DataLoader และ classification จริงด้วย PyTorch