ข้ามไปยังเนื้อหา
vibe-coder/academy
หลักสูตรทั้งหมด
MODULE 08

Manufacturing QC และ MLOps สำหรับสายการผลิต

ออกแบบโปรโตคอลทดสอบที่ปรับ FAR/FRR ด้วยการ optimize ROC แบบอิงต้นทุน, วงจร active learning รับมือ lighting drift, และกัน catastrophic forgetting ด้วย Elastic Weight Consolidation (EWC) + Experience Replay

  • ROC
  • FAR/FRR
  • Active Learning
  • EWC
  • Experience Replay

ในสายการผลิตจริง โมเดลที่ “accuracy สูง” ยังไม่พอ ต้องเลือกจุดทำงานที่สมดุลต้นทุนของการปล่อย ของเสียกับการทิ้งของดี ต้องตามให้ทันแสงที่เปลี่ยน และต้องเรียนรู้สิ่งใหม่โดยไม่ลืมของเก่า บทนี้คือชุดคำตอบ ระดับ production สำหรับสามคำถามนั้น

บริบท: QC ที่ต้นทุนไม่เท่ากัน

ของส่วนใหญ่เป็นของดี (class ไม่สมดุล) และต้นทุนของความผิดพลาดสองชนิดต่างกันมาก: การปล่อยของเสียถึงมือ ลูกค้า (false accept) มักแพงกว่าการทิ้งของดี (false reject) หลายเท่า ดังนั้น accuracy จึงหลอกตา เราต้อง ออกแบบโปรโตคอลทดสอบที่อิงต้นทุนจริง

ปรับ FAR vs FRR ด้วย cost-based ROC optimization

FAR (False Acceptance Rate) คือสัดส่วนของเสียที่ถูกปล่อยผ่าน FRR (False Rejection Rate) คือสัดส่วนของดีที่ถูกปฏิเสธ ทั้งสองเป็น trade-off ผ่าน threshold แทนที่จะเลือก จุดที่ accuracy สูงสุด เรากำหนดต้นทุนให้แต่ละชนิดความผิดพลาด แล้วเลือก threshold ที่ทำให้ต้นทุนรวมที่คาดหวังต่ำสุด

threshold.py
Python
1import numpy as np
2from sklearn.metrics import roc_curve
3
4
5def optimal_threshold(
6 y_true: np.ndarray, # 1 = defect (positive), 0 = ok
7 y_score: np.ndarray, # คะแนนความน่าจะเป็น defect
8 cost_false_accept: float = 50.0, # ปล่อยของเสีย (แพง)
9 cost_false_reject: float = 1.0, # ทิ้งของดี
10 defect_prior: float | None = None,
11) -> dict:
12 """เลือก threshold ที่ minimize expected cost บน ROC."""
13 fpr, tpr, thresholds = roc_curve(y_true, y_score)
14 # FRR = พลาด defect = 1 - tpr ; FAR ฝั่ง ok = fpr
15 frr = 1.0 - tpr
16 far = fpr
17
18 p_defect = defect_prior if defect_prior is not None else float(y_true.mean())
19 p_ok = 1.0 - p_defect
20
21 # expected cost ต่อชิ้น = ต้นทุน_พลาดdefect*P(defect)*FRR + ต้นทุน_ปล่อยเสีย*P(ok)*FAR
22 expected_cost = (
23 cost_false_reject * p_defect * frr
24 + cost_false_accept * p_ok * far
25 )
26 best = int(np.argmin(expected_cost))
27 return {
28 "threshold": float(thresholds[best]),
29 "far": float(far[best]),
30 "frr": float(frr[best]),
31 "expected_cost": float(expected_cost[best]),
32 }

Active learning loop รับมือ lighting drift

แสงในไลน์ผลิตเปลี่ยนตามเวลา/กะ ทำให้ distribution เลื่อน (lighting drift) การ label ทุกภาพใหม่แพงเกินไป active learning เลือกเฉพาะภาพที่โมเดล “ไม่มั่นใจที่สุด” (uncertainty sampling) มาให้คน label เพราะภาพเหล่านั้นให้ข้อมูลใหม่ต่อการเรียนรู้สูงสุดต่อหนึ่ง label

active_learning.py
Python
1import numpy as np
2
3
4def margin_uncertainty(probs: np.ndarray) -> np.ndarray:
5 """ความไม่มั่นใจแบบ margin: 1 - (top1 - top2). สูง = ไม่มั่นใจ."""
6 sorted_p = np.sort(probs, axis=1)[:, ::-1]
7 margin = sorted_p[:, 0] - sorted_p[:, 1]
8 return 1.0 - margin
9
10
11def select_for_labeling(
12 probs: np.ndarray, # (N, num_classes)
13 budget: int,
14 near_threshold: tuple[float, float] | None = None,
15 binary_score: np.ndarray | None = None,
16) -> np.ndarray:
17 """คืน index ของภาพที่ควรส่งให้คน label ภายใต้ budget ที่จำกัด."""
18 uncertainty = margin_uncertainty(probs)
19
20 # เน้นภาพที่คะแนนใกล้เส้นตัดสินใจ ซึ่งมักเป็นผลของ drift
21 if near_threshold is not None and binary_score is not None:
22 lo, hi = near_threshold
23 in_band = (binary_score >= lo) & (binary_score <= hi)
24 uncertainty = np.where(in_band, uncertainty + 1.0, uncertainty)
25
26 return np.argsort(uncertainty)[::-1][:budget]

กัน catastrophic forgetting: EWC + Experience Replay

เมื่อเทรนต่อด้วยข้อมูลใหม่ล้วน โมเดลมักลืม task เดิม (catastrophic forgetting) สองเทคนิคที่ใช้คู่กันได้ผลดี: EWC เพิ่ม regularization ลงโทษการขยับ weight ที่สำคัญต่อ task เดิม (ถ่วงด้วย Fisher information) ส่วน Experience Replay ผสมตัวอย่างเก่าเข้าไปใน batch เพื่อเสริมความจำโดยตรง

ewc.py
Python
1import torch
2import torch.nn as nn
3
4
5class EWC:
6 """Elastic Weight Consolidation: ปกป้อง weight สำคัญของ task เดิม."""
7
8 def __init__(self, model: nn.Module, lambda_: float = 1000.0):
9 self.model = model
10 self.lambda_ = lambda_
11 # ค่า weight อ้างอิงหลังเรียน task เดิม (theta*)
12 self.star = {n: p.detach().clone() for n, p in model.named_parameters()}
13 self.fisher: dict[str, torch.Tensor] = {}
14
15 def compute_fisher(self, loader, device: str) -> None:
16 """ประมาณ Fisher information = ความสำคัญของแต่ละ weight ต่อ task เดิม."""
17 fisher = {n: torch.zeros_like(p) for n, p in self.model.named_parameters()}
18 self.model.eval()
19 for x, y in loader:
20 x, y = x.to(device), y.to(device)
21 self.model.zero_grad()
22 loss = nn.functional.cross_entropy(self.model(x), y)
23 loss.backward()
24 for n, p in self.model.named_parameters():
25 if p.grad is not None:
26 fisher[n] += p.grad.detach() ** 2
27 n_batches = max(len(loader), 1)
28 self.fisher = {n: f / n_batches for n, f in fisher.items()}
29
30 def penalty(self) -> torch.Tensor:
31 """loss term ที่ลงโทษการขยับ weight สำคัญออกจาก theta*."""
32 loss = torch.tensor(0.0, device=next(self.model.parameters()).device)
33 for n, p in self.model.named_parameters():
34 if n in self.fisher:
35 loss = loss + (self.fisher[n] * (p - self.star[n]) ** 2).sum()
36 return self.lambda_ * loss
train_continual.py
Python
1import random
2import torch
3import torch.nn as nn
4
5
6def train_step_continual(
7 model: nn.Module,
8 new_batch, # (x, y) จากข้อมูลใหม่
9 replay_buffer: list, # ตัวอย่างเก่าที่เก็บไว้
10 ewc: "EWC",
11 optimizer: torch.optim.Optimizer,
12 device: str,
13 replay_size: int = 16,
14) -> float:
15 """หนึ่งก้าวการเทรนต่อเนื่อง: ข้อมูลใหม่ + experience replay + EWC penalty."""
16 model.train()
17 x_new, y_new = (t.to(device) for t in new_batch)
18
19 # Experience Replay: ดึงตัวอย่างเก่ามาผสม กัน distribution เก่าหายไป
20 if replay_buffer:
21 sampled = random.sample(replay_buffer, min(replay_size, len(replay_buffer)))
22 x_old = torch.stack([s[0] for s in sampled]).to(device)
23 y_old = torch.tensor([s[1] for s in sampled]).to(device)
24 x = torch.cat([x_new, x_old]); y = torch.cat([y_new, y_old])
25 else:
26 x, y = x_new, y_new
27
28 optimizer.zero_grad()
29 loss = nn.functional.cross_entropy(model(x), y) + ewc.penalty()
30 loss.backward()
31 optimizer.step()
32 return float(loss.item())

เช็กลิสต์ก่อนสัมภาษณ์

  • อธิบาย FAR/FRR เป็น trade-off และเลือกจุดทำงานด้วย expected cost ไม่ใช่ accuracy
  • วาด ROC ได้ และชี้ว่าจุด optimal อยู่ที่ใดเมื่อต้นทุนสองด้านต่างกัน
  • อธิบาย uncertainty sampling ว่าเลือกภาพ confidence ต่ำ/entropy สูงเพื่อใช้ label budget คุ้มสุด
  • อธิบาย catastrophic forgetting และบอกได้ว่า EWC ใช้ Fisher information ถ่วงความสำคัญ weight
  • รู้ว่า EWC (ระดับ parameter) กับ Experience Replay (ระดับข้อมูล) เสริมกันอย่างไร

สรุปสำคัญ

  • ในงาน QC ต้นทุนของ false accept (ปล่อยของเสีย) กับ false reject (ทิ้งของดี) ไม่เท่ากัน เลือก threshold ที่ต้นทุนรวมต่ำสุดบน ROC ไม่ใช่ที่ accuracy สูงสุด
  • lighting drift ทำให้ distribution เปลี่ยน ใช้ active learning ดึงเฉพาะภาพที่โมเดลไม่มั่นใจ (uncertainty sampling) มาให้คน label เพื่อใช้ budget คุ้มที่สุด
  • การเทรนต่อด้วยข้อมูลใหม่ล้วน ทำให้ลืมของเก่า (catastrophic forgetting) EWC ลงโทษการเปลี่ยน weight สำคัญ ส่วน Experience Replay ผสมข้อมูลเก่าเข้าไปด้วย
ทดสอบความเข้าใจ

ควิซท้ายบท

0/4 ข้อ
  1. 01ในการตรวจ QC เครื่องประดับ ทำไมจึงไม่ควรเลือก threshold ที่ให้ accuracy สูงสุด

  2. 02FAR (False Acceptance Rate) และ FRR (False Rejection Rate) สัมพันธ์กันอย่างไรเมื่อปรับ threshold

  3. 03กลยุทธ์ active learning แบบ uncertainty sampling รับมือ lighting drift อย่างไรให้คุ้ม label budget

  4. 04Elastic Weight Consolidation (EWC) กัน catastrophic forgetting ด้วยหลักการใด

ตอบให้ครบทุกข้อแล้วกดส่งคำตอบเพื่อดูเฉลย